Showing preview only (4,597K chars total). Download the full file or copy to clipboard to get everything.
Repository: miniflux/v2
Branch: main
Commit: f03285883be2
Files: 619
Total size: 4.3 MB
Directory structure:
gitextract_58fdb7bz/
├── .devcontainer/
│ ├── devcontainer.json
│ └── docker-compose.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── documentation.yml
│ │ ├── feature_request.yml
│ │ ├── feed_issue.yml
│ │ └── proposal.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build_binaries.yml
│ ├── codeberg_mirror.yml
│ ├── codeql-analysis.yml
│ ├── debian_packages.yml
│ ├── docker.yml
│ ├── linters.yml
│ ├── rpm_packages.yml
│ ├── scripts/
│ │ └── commit-checker.py
│ └── tests.yml
├── .gitignore
├── .golangci.yml
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── Procfile
├── README.md
├── SECURITY.md
├── client/
│ ├── README.md
│ ├── client.go
│ ├── client_test.go
│ ├── doc.go
│ ├── model.go
│ ├── options.go
│ └── request.go
├── contrib/
│ ├── README.md
│ ├── ansible/
│ │ ├── inventories/
│ │ │ └── group_vars/
│ │ │ └── miniflux_vars.yml
│ │ ├── playbooks/
│ │ │ └── playbook.yml
│ │ └── roles/
│ │ └── mgrote.miniflux/
│ │ ├── README.md
│ │ ├── defaults/
│ │ │ └── main.yml
│ │ ├── handlers/
│ │ │ └── main.yml
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ └── templates/
│ │ └── miniflux.conf
│ ├── bruno/
│ │ ├── README.md
│ │ └── miniflux/
│ │ ├── Bookmark an entry.bru
│ │ ├── Create a feed.bru
│ │ ├── Create a new category.bru
│ │ ├── Create a new user.bru
│ │ ├── Delete a category.bru
│ │ ├── Delete a feed.bru
│ │ ├── Delete a user.bru
│ │ ├── Discover feeds.bru
│ │ ├── Fetch entry website content.bru
│ │ ├── Flush history.bru
│ │ ├── Get a single entry.bru
│ │ ├── Get a single feed entry.bru
│ │ ├── Get a single feed.bru
│ │ ├── Get a single user by ID.bru
│ │ ├── Get a single user by username.bru
│ │ ├── Get all categories.bru
│ │ ├── Get all entries.bru
│ │ ├── Get all feeds.bru
│ │ ├── Get all users.bru
│ │ ├── Get category entries.bru
│ │ ├── Get category entry.bru
│ │ ├── Get category feeds.bru
│ │ ├── Get current user.bru
│ │ ├── Get feed counters.bru
│ │ ├── Get feed entries.bru
│ │ ├── Get feed icon by feed ID.bru
│ │ ├── Get feed icon by icon ID.bru
│ │ ├── Get version and build information.bru
│ │ ├── Mark all category entries as read.bru
│ │ ├── Mark all user entries as read.bru
│ │ ├── Mark feed as read.bru
│ │ ├── OPML Export.bru
│ │ ├── OPML Import.bru
│ │ ├── Refresh a single feed.bru
│ │ ├── Refresh all feeds.bru
│ │ ├── Refresh category feeds.bru
│ │ ├── Save an entry.bru
│ │ ├── Update a category.bru
│ │ ├── Update a feed.bru
│ │ ├── Update a user.bru
│ │ ├── Update entries status.bru
│ │ ├── Update entry.bru
│ │ ├── bruno.json
│ │ └── environments/
│ │ └── Local.bru
│ ├── docker-compose/
│ │ ├── Caddyfile
│ │ ├── README.md
│ │ ├── basic.yml
│ │ ├── caddy.yml
│ │ └── traefik.yml
│ ├── grafana/
│ │ ├── README.md
│ │ └── dashboard.json
│ ├── sysvinit/
│ │ ├── README.md
│ │ └── etc/
│ │ ├── default/
│ │ │ └── miniflux
│ │ └── init.d/
│ │ └── miniflux
│ └── thunder_client/
│ ├── README.md
│ └── collection.json
├── go.mod
├── go.sum
├── internal/
│ ├── api/
│ │ ├── api.go
│ │ ├── api_integration_test.go
│ │ ├── api_key_handlers.go
│ │ ├── api_test.go
│ │ ├── category_handlers.go
│ │ ├── enclosure_handlers.go
│ │ ├── entry_handlers.go
│ │ ├── feed_handlers.go
│ │ ├── icon_handlers.go
│ │ ├── messages.go
│ │ ├── middleware.go
│ │ ├── opml_handlers.go
│ │ ├── subscription_handlers.go
│ │ ├── user_handlers.go
│ │ └── version_handler.go
│ ├── cli/
│ │ ├── ask_credentials.go
│ │ ├── cleanup_tasks.go
│ │ ├── cli.go
│ │ ├── create_admin.go
│ │ ├── daemon.go
│ │ ├── export_feeds.go
│ │ ├── flush_sessions.go
│ │ ├── health_check.go
│ │ ├── info.go
│ │ ├── logger.go
│ │ ├── refresh_feeds.go
│ │ ├── reset_password.go
│ │ └── scheduler.go
│ ├── config/
│ │ ├── config.go
│ │ ├── options.go
│ │ ├── options_parsing_test.go
│ │ ├── parser.go
│ │ ├── parser_test.go
│ │ ├── validators.go
│ │ └── validators_test.go
│ ├── crypto/
│ │ └── crypto.go
│ ├── database/
│ │ ├── database.go
│ │ ├── migrations.go
│ │ └── postgresql.go
│ ├── fever/
│ │ ├── README.md
│ │ ├── handler.go
│ │ ├── middleware.go
│ │ └── response.go
│ ├── googlereader/
│ │ ├── README.md
│ │ ├── handler.go
│ │ ├── item.go
│ │ ├── item_test.go
│ │ ├── middleware.go
│ │ ├── parameters.go
│ │ ├── prefix_suffix.go
│ │ ├── request_modifier.go
│ │ ├── response.go
│ │ └── stream.go
│ ├── http/
│ │ ├── client/
│ │ │ ├── client.go
│ │ │ └── client_test.go
│ │ ├── cookie/
│ │ │ └── cookie.go
│ │ ├── request/
│ │ │ ├── client_ip.go
│ │ │ ├── client_ip_test.go
│ │ │ ├── context.go
│ │ │ ├── context_test.go
│ │ │ ├── cookie.go
│ │ │ ├── cookie_test.go
│ │ │ ├── params.go
│ │ │ └── params_test.go
│ │ ├── response/
│ │ │ ├── builder.go
│ │ │ ├── builder_test.go
│ │ │ ├── html.go
│ │ │ ├── html_test.go
│ │ │ ├── json.go
│ │ │ ├── json_test.go
│ │ │ ├── response.go
│ │ │ ├── response_test.go
│ │ │ ├── text.go
│ │ │ ├── text_test.go
│ │ │ ├── xml.go
│ │ │ └── xml_test.go
│ │ └── server/
│ │ ├── healthcheck.go
│ │ ├── httpd.go
│ │ ├── metrics.go
│ │ ├── middleware.go
│ │ └── routes.go
│ ├── integration/
│ │ ├── apprise/
│ │ │ └── apprise.go
│ │ ├── archiveorg/
│ │ │ └── archiveorg.go
│ │ ├── betula/
│ │ │ └── betula.go
│ │ ├── cubox/
│ │ │ └── cubox.go
│ │ ├── discord/
│ │ │ └── discord.go
│ │ ├── espial/
│ │ │ └── espial.go
│ │ ├── instapaper/
│ │ │ └── instapaper.go
│ │ ├── integration.go
│ │ ├── integration_test.go
│ │ ├── karakeep/
│ │ │ └── karakeep.go
│ │ ├── linkace/
│ │ │ └── linkace.go
│ │ ├── linkding/
│ │ │ └── linkding.go
│ │ ├── linktaco/
│ │ │ ├── linktaco.go
│ │ │ └── linktaco_test.go
│ │ ├── linkwarden/
│ │ │ ├── linkwarden.go
│ │ │ └── linkwarden_test.go
│ │ ├── matrixbot/
│ │ │ ├── client.go
│ │ │ └── matrixbot.go
│ │ ├── notion/
│ │ │ └── notion.go
│ │ ├── ntfy/
│ │ │ └── ntfy.go
│ │ ├── nunuxkeeper/
│ │ │ └── nunuxkeeper.go
│ │ ├── omnivore/
│ │ │ └── omnivore.go
│ │ ├── pinboard/
│ │ │ ├── pinboard.go
│ │ │ └── post.go
│ │ ├── pushover/
│ │ │ └── pushover.go
│ │ ├── raindrop/
│ │ │ └── raindrop.go
│ │ ├── readeck/
│ │ │ ├── readeck.go
│ │ │ └── readeck_test.go
│ │ ├── readwise/
│ │ │ └── readwise.go
│ │ ├── rssbridge/
│ │ │ └── rssbridge.go
│ │ ├── shaarli/
│ │ │ └── shaarli.go
│ │ ├── shiori/
│ │ │ └── shiori.go
│ │ ├── slack/
│ │ │ └── slack.go
│ │ ├── telegrambot/
│ │ │ ├── client.go
│ │ │ └── telegrambot.go
│ │ ├── wallabag/
│ │ │ ├── wallabag.go
│ │ │ └── wallabag_test.go
│ │ └── webhook/
│ │ └── webhook.go
│ ├── locale/
│ │ ├── catalog.go
│ │ ├── catalog_test.go
│ │ ├── error.go
│ │ ├── error_test.go
│ │ ├── locale.go
│ │ ├── locale_test.go
│ │ ├── plural.go
│ │ ├── plural_test.go
│ │ ├── printer.go
│ │ ├── printer_test.go
│ │ └── translations/
│ │ ├── ar_SA.json
│ │ ├── de_DE.json
│ │ ├── el_EL.json
│ │ ├── en_US.json
│ │ ├── es_ES.json
│ │ ├── fi_FI.json
│ │ ├── fr_FR.json
│ │ ├── gl_ES.json
│ │ ├── hi_IN.json
│ │ ├── id_ID.json
│ │ ├── it_IT.json
│ │ ├── ja_JP.json
│ │ ├── nan_Latn_pehoeji.json
│ │ ├── nl_NL.json
│ │ ├── pl_PL.json
│ │ ├── pt_BR.json
│ │ ├── ro_RO.json
│ │ ├── ru_RU.json
│ │ ├── tr_TR.json
│ │ ├── uk_UA.json
│ │ ├── zh_CN.json
│ │ └── zh_TW.json
│ ├── mediaproxy/
│ │ ├── media_proxy_test.go
│ │ ├── rewriter.go
│ │ └── url.go
│ ├── metric/
│ │ └── metric.go
│ ├── model/
│ │ ├── api_key.go
│ │ ├── app_session.go
│ │ ├── categories_sort_options.go
│ │ ├── category.go
│ │ ├── enclosure.go
│ │ ├── enclosure_test.go
│ │ ├── entry.go
│ │ ├── feed.go
│ │ ├── feed_test.go
│ │ ├── home_page.go
│ │ ├── icon.go
│ │ ├── integration.go
│ │ ├── job.go
│ │ ├── model.go
│ │ ├── subscription.go
│ │ ├── theme.go
│ │ ├── user.go
│ │ ├── user_session.go
│ │ └── webauthn.go
│ ├── oauth2/
│ │ ├── authorization.go
│ │ ├── google.go
│ │ ├── manager.go
│ │ ├── oidc.go
│ │ ├── profile.go
│ │ └── provider.go
│ ├── proxyrotator/
│ │ ├── proxyrotator.go
│ │ └── proxyrotator_test.go
│ ├── reader/
│ │ ├── atom/
│ │ │ ├── atom_03.go
│ │ │ ├── atom_03_adapter.go
│ │ │ ├── atom_03_test.go
│ │ │ ├── atom_10.go
│ │ │ ├── atom_10_adapter.go
│ │ │ ├── atom_10_test.go
│ │ │ ├── atom_common.go
│ │ │ └── parser.go
│ │ ├── date/
│ │ │ ├── parser.go
│ │ │ └── parser_test.go
│ │ ├── dublincore/
│ │ │ └── dublincore.go
│ │ ├── encoding/
│ │ │ ├── encoding.go
│ │ │ ├── encoding_test.go
│ │ │ └── testdata/
│ │ │ ├── invalid-prolog.xml
│ │ │ ├── iso-8859-1-meta-after-1024.html
│ │ │ ├── iso-8859-1.html
│ │ │ ├── iso-8859-1.xml
│ │ │ ├── koi8r.xml
│ │ │ ├── utf8-incorrect-prolog.xml
│ │ │ ├── utf8-meta-after-1024.html
│ │ │ ├── utf8.html
│ │ │ ├── utf8.xml
│ │ │ ├── windows-1252-incorrect-prolog.xml
│ │ │ └── windows-1252.xml
│ │ ├── fetcher/
│ │ │ ├── encoding_wrappers.go
│ │ │ ├── request_builder.go
│ │ │ ├── request_builder_test.go
│ │ │ ├── response_handler.go
│ │ │ └── response_handler_test.go
│ │ ├── filter/
│ │ │ ├── filter.go
│ │ │ └── filter_test.go
│ │ ├── googleplay/
│ │ │ └── googleplay.go
│ │ ├── handler/
│ │ │ └── handler.go
│ │ ├── icon/
│ │ │ ├── checker.go
│ │ │ ├── finder.go
│ │ │ └── finder_test.go
│ │ ├── itunes/
│ │ │ └── itunes.go
│ │ ├── json/
│ │ │ ├── adapter.go
│ │ │ ├── json.go
│ │ │ ├── parser.go
│ │ │ └── parser_test.go
│ │ ├── media/
│ │ │ ├── media.go
│ │ │ └── media_test.go
│ │ ├── opml/
│ │ │ ├── handler.go
│ │ │ ├── opml.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ ├── serializer.go
│ │ │ ├── serializer_test.go
│ │ │ └── subscription.go
│ │ ├── parser/
│ │ │ ├── format.go
│ │ │ ├── format_test.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ └── testdata/
│ │ │ ├── encoding_ISO-8859-1.xml
│ │ │ ├── encoding_WINDOWS-1251.xml
│ │ │ ├── large_atom.xml
│ │ │ ├── large_rss.xml
│ │ │ ├── no_encoding_ISO-8859-1.xml
│ │ │ ├── rdf_UTF8.xml
│ │ │ ├── small_atom.xml
│ │ │ └── urdu_UTF8.xml
│ │ ├── processor/
│ │ │ ├── bilibili.go
│ │ │ ├── nebula.go
│ │ │ ├── odysee.go
│ │ │ ├── processor.go
│ │ │ ├── reading_time.go
│ │ │ ├── utils.go
│ │ │ ├── utils_test.go
│ │ │ ├── youtube.go
│ │ │ └── youtube_test.go
│ │ ├── rdf/
│ │ │ ├── adapter.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ └── rdf.go
│ │ ├── readability/
│ │ │ ├── readability.go
│ │ │ └── readability_test.go
│ │ ├── readingtime/
│ │ │ ├── readingtime.go
│ │ │ └── readingtime_test.go
│ │ ├── rewrite/
│ │ │ ├── content_rewrite.go
│ │ │ ├── content_rewrite_functions.go
│ │ │ ├── content_rewrite_rules.go
│ │ │ ├── content_rewrite_test.go
│ │ │ ├── referer_override.go
│ │ │ ├── referer_override_test.go
│ │ │ ├── url_rewrite.go
│ │ │ └── url_rewrite_test.go
│ │ ├── rss/
│ │ │ ├── adapter.go
│ │ │ ├── atom.go
│ │ │ ├── feedburner.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ ├── podcast.go
│ │ │ └── rss.go
│ │ ├── sanitizer/
│ │ │ ├── sanitizer.go
│ │ │ ├── sanitizer_test.go
│ │ │ ├── srcset.go
│ │ │ ├── srcset_test.go
│ │ │ ├── strip_tags.go
│ │ │ ├── strip_tags_test.go
│ │ │ ├── testdata/
│ │ │ │ ├── miniflux_github.html
│ │ │ │ └── miniflux_wikipedia.html
│ │ │ ├── truncate.go
│ │ │ └── truncate_test.go
│ │ ├── scraper/
│ │ │ ├── rules.go
│ │ │ ├── scraper.go
│ │ │ ├── scraper_test.go
│ │ │ └── testdata/
│ │ │ ├── iframe.html
│ │ │ ├── iframe.html-result
│ │ │ ├── img.html
│ │ │ ├── img.html-result
│ │ │ ├── p.html
│ │ │ └── p.html-result
│ │ ├── subscription/
│ │ │ ├── finder.go
│ │ │ ├── finder_test.go
│ │ │ └── subscription.go
│ │ ├── urlcleaner/
│ │ │ ├── urlcleaner.go
│ │ │ └── urlcleaner_test.go
│ │ └── xml/
│ │ ├── decoder.go
│ │ ├── decoder_test.go
│ │ └── testdata/
│ │ ├── iso88591.xml
│ │ ├── iso88591_utf8_mismatch.xml
│ │ └── koi8r.xml
│ ├── storage/
│ │ ├── api_key.go
│ │ ├── batch.go
│ │ ├── category.go
│ │ ├── certificate_cache.go
│ │ ├── enclosure.go
│ │ ├── entry.go
│ │ ├── entry_pagination_builder.go
│ │ ├── entry_query_builder.go
│ │ ├── entry_test.go
│ │ ├── feed.go
│ │ ├── feed_query_builder.go
│ │ ├── icon.go
│ │ ├── integration.go
│ │ ├── session.go
│ │ ├── storage.go
│ │ ├── user.go
│ │ ├── user_session.go
│ │ └── webauthn.go
│ ├── systemd/
│ │ └── systemd.go
│ ├── template/
│ │ ├── engine.go
│ │ ├── functions.go
│ │ ├── functions_test.go
│ │ └── templates/
│ │ ├── common/
│ │ │ ├── feed_list.html
│ │ │ ├── feed_menu.html
│ │ │ ├── item_meta.html
│ │ │ ├── layout.html
│ │ │ ├── pagination.html
│ │ │ └── settings_menu.html
│ │ └── views/
│ │ ├── about.html
│ │ ├── add_subscription.html
│ │ ├── api_keys.html
│ │ ├── categories.html
│ │ ├── category_entries.html
│ │ ├── category_feeds.html
│ │ ├── choose_subscription.html
│ │ ├── create_api_key.html
│ │ ├── create_category.html
│ │ ├── create_user.html
│ │ ├── edit_category.html
│ │ ├── edit_feed.html
│ │ ├── edit_user.html
│ │ ├── entry.html
│ │ ├── feed_entries.html
│ │ ├── feeds.html
│ │ ├── history_entries.html
│ │ ├── import.html
│ │ ├── integrations.html
│ │ ├── login.html
│ │ ├── offline.html
│ │ ├── search.html
│ │ ├── sessions.html
│ │ ├── settings.html
│ │ ├── shared_entries.html
│ │ ├── starred_entries.html
│ │ ├── tag_entries.html
│ │ ├── unread_entries.html
│ │ ├── users.html
│ │ └── webauthn_rename.html
│ ├── timezone/
│ │ ├── timezone.go
│ │ └── timezone_test.go
│ ├── ui/
│ │ ├── about.go
│ │ ├── api_key_create.go
│ │ ├── api_key_list.go
│ │ ├── api_key_remove.go
│ │ ├── api_key_save.go
│ │ ├── category_create.go
│ │ ├── category_edit.go
│ │ ├── category_entries.go
│ │ ├── category_entries_all.go
│ │ ├── category_entries_starred.go
│ │ ├── category_feeds.go
│ │ ├── category_list.go
│ │ ├── category_mark_as_read.go
│ │ ├── category_refresh.go
│ │ ├── category_remove.go
│ │ ├── category_remove_feed.go
│ │ ├── category_save.go
│ │ ├── category_update.go
│ │ ├── entry_category.go
│ │ ├── entry_enclosure_save_position.go
│ │ ├── entry_feed.go
│ │ ├── entry_read.go
│ │ ├── entry_save.go
│ │ ├── entry_scraper.go
│ │ ├── entry_search.go
│ │ ├── entry_starred.go
│ │ ├── entry_tag.go
│ │ ├── entry_toggle_starred.go
│ │ ├── entry_unread.go
│ │ ├── entry_update_status.go
│ │ ├── feed_edit.go
│ │ ├── feed_entries.go
│ │ ├── feed_entries_all.go
│ │ ├── feed_icon.go
│ │ ├── feed_list.go
│ │ ├── feed_mark_as_read.go
│ │ ├── feed_refresh.go
│ │ ├── feed_remove.go
│ │ ├── feed_update.go
│ │ ├── form/
│ │ │ ├── api_key.go
│ │ │ ├── auth.go
│ │ │ ├── category.go
│ │ │ ├── feed.go
│ │ │ ├── integration.go
│ │ │ ├── settings.go
│ │ │ ├── settings_test.go
│ │ │ ├── subscription.go
│ │ │ ├── user.go
│ │ │ └── webauthn.go
│ │ ├── handler.go
│ │ ├── history_entries.go
│ │ ├── history_flush.go
│ │ ├── integration_show.go
│ │ ├── integration_update.go
│ │ ├── login_check.go
│ │ ├── login_show.go
│ │ ├── logout.go
│ │ ├── middleware.go
│ │ ├── oauth2.go
│ │ ├── oauth2_callback.go
│ │ ├── oauth2_redirect.go
│ │ ├── oauth2_unlink.go
│ │ ├── offline.go
│ │ ├── opml_export.go
│ │ ├── opml_import.go
│ │ ├── opml_upload.go
│ │ ├── pagination.go
│ │ ├── proxy.go
│ │ ├── search.go
│ │ ├── session/
│ │ │ └── session.go
│ │ ├── session_list.go
│ │ ├── session_remove.go
│ │ ├── settings_show.go
│ │ ├── settings_update.go
│ │ ├── share.go
│ │ ├── shared_entries.go
│ │ ├── starred_entries.go
│ │ ├── starred_entry_category.go
│ │ ├── static/
│ │ │ ├── css/
│ │ │ │ ├── common.css
│ │ │ │ ├── dark.css
│ │ │ │ ├── light.css
│ │ │ │ ├── sans_serif.css
│ │ │ │ ├── serif.css
│ │ │ │ └── system.css
│ │ │ ├── js/
│ │ │ │ ├── .eslintrc.json
│ │ │ │ ├── .jshintrc
│ │ │ │ ├── app.js
│ │ │ │ ├── keyboard_handler.js
│ │ │ │ ├── service_worker.js
│ │ │ │ ├── touch_handler.js
│ │ │ │ └── webauthn_handler.js
│ │ │ └── static.go
│ │ ├── static_app_icon.go
│ │ ├── static_favicon.go
│ │ ├── static_javascript.go
│ │ ├── static_manifest.go
│ │ ├── static_stylesheet.go
│ │ ├── subscription_add.go
│ │ ├── subscription_bookmarklet.go
│ │ ├── subscription_choose.go
│ │ ├── subscription_submit.go
│ │ ├── tag_entries_all.go
│ │ ├── ui.go
│ │ ├── unread_entries.go
│ │ ├── unread_entry_category.go
│ │ ├── unread_entry_feed.go
│ │ ├── unread_mark_all_read.go
│ │ ├── user_create.go
│ │ ├── user_edit.go
│ │ ├── user_list.go
│ │ ├── user_remove.go
│ │ ├── user_save.go
│ │ ├── user_update.go
│ │ ├── view/
│ │ │ └── view.go
│ │ └── webauthn.go
│ ├── urllib/
│ │ ├── url.go
│ │ └── url_test.go
│ ├── validator/
│ │ ├── api_key.go
│ │ ├── category.go
│ │ ├── enclosure.go
│ │ ├── enclosure_test.go
│ │ ├── entry.go
│ │ ├── entry_test.go
│ │ ├── feed.go
│ │ ├── filter.go
│ │ ├── filter_test.go
│ │ ├── subscription.go
│ │ ├── subscription_test.go
│ │ ├── user.go
│ │ ├── user_test.go
│ │ ├── validator.go
│ │ └── validator_test.go
│ ├── version/
│ │ ├── version.go
│ │ └── version_test.go
│ └── worker/
│ ├── pool.go
│ └── worker.go
├── main.go
├── miniflux.1
└── packaging/
├── debian/
│ ├── Dockerfile
│ ├── build.sh
│ ├── compat
│ ├── control
│ ├── copyright
│ ├── miniflux.dirs
│ ├── miniflux.manpages
│ ├── miniflux.postinst
│ └── rules
├── docker/
│ ├── alpine/
│ │ └── Dockerfile
│ └── distroless/
│ └── Dockerfile
├── miniflux.conf
├── rpm/
│ ├── Dockerfile
│ └── miniflux.spec
└── systemd/
└── miniflux.service
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "Miniflux",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"remoteUser": "vscode",
"forwardPorts": [
8080
],
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"moby": false
}
},
"customizations": {
"vscode": {
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
"extensions": [
"ms-azuretools.vscode-docker",
"golang.go",
"rangav.vscode-thunder-client",
"GitHub.codespaces",
"GitHub.copilot",
"GitHub.copilot-chat"
]
}
}
}
================================================
FILE: .devcontainer/docker-compose.yml
================================================
services:
app:
image: mcr.microsoft.com/devcontainers/go:1-trixie # https://www.debian.org/releases/trixie/index.en.html
volumes:
- ..:/workspace:cached
command: sleep infinity
network_mode: service:db
environment:
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql
hostname: postgres
environment:
POSTGRES_DB: miniflux2
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
apprise:
image: caronc/apprise:1.0
restart: unless-stopped
hostname: apprise
volumes:
postgres-data: null
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: "Bug Report"
description: "Report a bug or unexpected behavior"
title: "[Bug]: "
type: "Bug"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug! Please provide detailed information to help us reproduce and fix the issue.
- type: input
id: summary
attributes:
label: "Bug Summary"
description: "Briefly describe the bug."
placeholder: "e.g., Error when saving a new entry"
validations:
required: true
- type: textarea
id: description
attributes:
label: "Description"
description: "A clear and concise description of the bug."
placeholder: "e.g., When I click 'Save', I get a 500 error."
validations:
required: true
- type: textarea
id: steps_to_reproduce
attributes:
label: "Steps to Reproduce"
description: "Steps to reproduce the behavior."
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: "Expected Behavior"
description: "What should happen instead?"
placeholder: "e.g., The form should be saved successfully."
validations:
required: true
- type: textarea
id: actual_behavior
attributes:
label: "Actual Behavior"
description: "What actually happens?"
placeholder: "e.g., A 500 error is returned with no useful error message."
validations:
required: true
- type: input
id: version
attributes:
label: "Version"
description: "Which version of Miniflux are you using?"
placeholder: "e.g., 2.2.6"
validations:
required: true
- type: input
id: browser
attributes:
label: "Browser"
description: "If applicable, which browser are you using? Please provide the version."
placeholder: "e.g., Chrome, Firefox, Safari"
validations:
required: false
- type: textarea
id: logs
attributes:
label: "Relevant Logs or Error Output"
description: "Paste any relevant logs or error messages (if applicable)."
render: shell
placeholder: "e.g., Stack trace, log files, browser console logs, or console output"
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context about the problem here."
placeholder: "e.g., Screenshots, video recordings, or related issues"
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have searched existing issues to ensure this bug hasn't been reported before."
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/documentation.yml
================================================
name: "Documentation Issue"
description: "Report issues or suggest improvements for the documentation"
title: "[Docs]: "
type: "Documentation"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for helping improve the Miniflux documentation! Clear and accurate documentation helps everyone.
- type: dropdown
id: issue_type
attributes:
label: "Documentation Issue Type"
description: "What kind of documentation issue are you reporting?"
options:
- "Missing Information"
- "Incorrect Information"
- "Outdated Information"
- "Unclear Explanation"
- "Formatting/Structural Issue"
- "Typo/Grammar Error"
- "Documentation Request"
- "Other"
validations:
required: true
- type: input
id: summary
attributes:
label: "Summary"
description: "Briefly describe the documentation issue."
placeholder: "e.g., The API authentication section is outdated"
validations:
required: true
- type: input
id: location
attributes:
label: "Location"
description: "Where is the documentation you're referring to? Provide URLs, file paths, or section names."
placeholder: "e.g., README.md, docs/api.md, Installation section of the website"
validations:
required: true
- type: textarea
id: description
attributes:
label: "Detailed Description"
description: "Provide a detailed description of the issue or improvement."
placeholder: "e.g., The API authentication section doesn't mention the new token-based authentication method introduced in version 2.0.5."
validations:
required: true
- type: textarea
id: current_content
attributes:
label: "Current Content (if applicable)"
description: "What does the current documentation say?"
placeholder: "Paste the current documentation text here."
validations:
required: false
- type: textarea
id: suggested_content
attributes:
label: "Suggested Changes"
description: "If you have specific suggestions for how to improve the documentation, please provide them here."
placeholder: "e.g., Add a new section about token-based authentication with these details..."
validations:
required: false
- type: input
id: version
attributes:
label: "Version"
description: "Which version of Miniflux does this documentation issue relate to?"
placeholder: "e.g., 2.2.6, or 'all versions'"
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: "Feature Request"
description: "Suggest an idea or improvement for the project"
title: "[Feature]: "
type: "Feature"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a feature! Please provide detailed information to help us understand and evaluate your idea.
- type: input
id: summary
attributes:
label: "Feature Summary"
description: "Briefly describe the feature or enhancement."
placeholder: "e.g., Add dark mode support"
validations:
required: true
- type: textarea
id: problem
attributes:
label: "What problem does this feature solve?"
description: "Explain the problem or limitation this feature would address."
placeholder: "e.g., It's difficult to use the app in low-light environments."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Proposed Solution"
description: "Describe how you think this feature should work."
placeholder: "e.g., Add a toggle in settings to switch between light and dark mode."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Alternatives Considered"
description: "Have you considered other solutions or workarounds?"
placeholder: "e.g., Using browser extensions to force dark mode."
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context, screenshots, or examples to explain your request."
placeholder: "e.g., A screenshot of a similar feature in another project."
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have searched existing issues to ensure this feature hasn't been requested before."
required: true
- label: "I understand that feature requests are not guaranteed to be implemented."
required: true
- label: "I agree to follow the project's contribution guidelines."
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/feed_issue.yml
================================================
name: "Feed/Website Issue"
description: "Report problems with a specific feed or website"
title: "[Feed Issue]: "
type: "Feed Issue"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting an issue with a feed or website! Please provide detailed information to help us diagnose and resolve the problem.
- type: input
id: feed_url
attributes:
label: "Feed URL"
description: "Provide the URL of the feed that is not working correctly."
placeholder: "e.g., https://example.com/feed.xml"
validations:
required: true
- type: input
id: website_url
attributes:
label: "Website URL"
description: "Provide the URL of the website."
placeholder: "e.g., https://example.com"
validations:
required: true
- type: textarea
id: problem_description
attributes:
label: "Problem Description"
description: "Describe the issue you are experiencing with this feed."
placeholder: |
e.g.,
- The feed URL returns a 403 error.
- The content is malformed.
- Images are not loading in the web ui.
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: "Expected Behavior"
description: "Describe what you expect to happen."
placeholder: "e.g., The feed should show the images correctly."
validations:
required: true
- type: textarea
id: error_logs
attributes:
label: "Relevant Logs or Error Output"
description: "Paste any relevant logs or error messages, if available."
render: shell
placeholder: "e.g., HTTP error codes, invalid XML warnings, etc."
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context, screenshots, or related information to help us troubleshoot."
placeholder: "e.g., Is this a recurring problem? Did the feed work before?"
validations:
required: false
- type: checkboxes
id: troubleshooting
attributes:
label: "Troubleshooting Steps"
description: "Please confirm that you have tried the following:"
options:
- label: "I have checked if the feed URL is correct and accessible in a web browser."
required: true
- label: "I have checked if the feed URL is correct and accessible with `curl`."
required: true
- label: "I have verified that the feed is valid using an RSS/Atom validator."
required: false
- label: "I have searched for existing issues to avoid duplicates."
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/proposal.yml
================================================
name: "Proposal / RFC"
description: "Propose a significant change, or architectural decision"
title: "[Proposal]: "
type: "Proposal"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to submit a proposal! Please provide detailed information to ensure a productive discussion.
- type: input
id: summary
attributes:
label: "Proposal Summary"
description: "A brief summary of the proposed change or idea."
placeholder: "e.g., Refactor database schema for performance optimization"
validations:
required: true
- type: textarea
id: motivation
attributes:
label: "Motivation and Context"
description: "Explain the problem this proposal addresses. Why is it necessary? What are the current limitations or pain points?"
placeholder: |
e.g.,
- The current database schema causes performance bottlenecks when querying large datasets.
- Adding this feature will improve scalability and reliability for large-scale use cases.
validations:
required: true
- type: textarea
id: proposed_solution
attributes:
label: "Proposed Solution"
description: "Describe the proposed solution or approach. Include technical details, diagrams, and examples where possible."
placeholder: |
e.g.,
- Redesign the schema to normalize tables and introduce indexing.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Alternatives Considered"
description: "List any alternative approaches that were considered and explain why they were rejected."
placeholder: |
e.g.,
- Use Redis for caching, but it adds operational complexity.
- Stick with the current schema and optimize queries, but this has limited impact on performance.
validations:
required: false
- type: textarea
id: impact
attributes:
label: "Impact and Risks"
description: "Describe the potential impact of this change. Highlight possible risks and backward compatibility concerns."
placeholder: |
e.g.,
- May require data migration with downtime.
- Could introduce breaking changes in API responses.
- Affects core functionality, requiring extensive testing.
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: "Additional Context or References"
description: "Add any relevant context, links to related discussions, RFCs, or design documents."
placeholder: "e.g., Links to research, GitHub issues, or similar projects"
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have reviewed existing proposals to ensure this change hasn't been proposed before."
required: true
- label: "I agree to provide follow-up updates and maintain discussion on this proposal."
required: true
- label: "I agree to follow the project's contribution guidelines."
required: true
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/packaging/docker/alpine"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/packaging/docker/distroless"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "packaging/debian"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "packaging/rpm"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/pull_request_template.md
================================================
Have you followed these guidelines?
- [ ] I have tested my changes
- [ ] There are no breaking changes
- [ ] I have thoroughly tested my changes and verified there are no regressions
- [ ] My commit messages follow the [Conventional Commits specification](https://www.conventionalcommits.org/)
- [ ] I have read and understood the [contribution guidelines](https://github.com/miniflux/v2/blob/main/CONTRIBUTING.md)
================================================
FILE: .github/workflows/build_binaries.yml
================================================
name: Build Binaries
permissions:
contents: read
on:
workflow_dispatch:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
name: Build
if: github.repository_owner == 'miniflux'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Golang
uses: actions/setup-go@v6
with:
go-version: stable
check-latest: true
- name: Compile binaries
env:
CGO_ENABLED: 0
run: make build
- name: Upload binaries
uses: actions/upload-artifact@v7
with:
name: binaries
path: miniflux-*
if-no-files-found: error
retention-days: 5
================================================
FILE: .github/workflows/codeberg_mirror.yml
================================================
name: Mirror to Codeberg
on:
push:
branches: [ main ]
delete:
workflow_dispatch:
jobs:
mirror:
if: github.repository_owner == 'miniflux'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Mirror to Codeberg
env:
CODEBERG_USERNAME: ${{ secrets.CODEBERG_USERNAME }}
CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
run: |
git remote add codeberg https://${{ secrets.CODEBERG_USERNAME }}:${{ secrets.CODEBERG_TOKEN }}@codeberg.org/miniflux/v2.git
git push --force --prune codeberg \
"refs/heads/*:refs/heads/*" \
"refs/tags/*:refs/tags/*"
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "CodeQL"
permissions: read-all
on:
push:
branches: [ main ]
paths:
- '**.js'
- '**.go'
- '!**_test.go'
- '.github/workflows/codeql-analysis.yml'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
paths:
- '**.js'
- '**.go'
- '!**_test.go'
- '.github/workflows/codeql-analysis.yml'
schedule:
- cron: '45 22 * * 3'
workflow_dispatch:
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/setup-go@v6
if: matrix.language == 'go'
with:
go-version: stable
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
================================================
FILE: .github/workflows/debian_packages.yml
================================================
name: Debian Packages
permissions: read-all
on:
workflow_dispatch:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
schedule:
- cron: '0 0 * * 1,4' # Runs at 00:00 UTC on Monday and Thursday
pull_request:
branches: [ main ]
paths:
- 'packaging/debian/**' # Only run on changes to the debian packaging files
jobs:
test-packages:
if: (github.event_name == 'schedule' && github.repository_owner == 'miniflux' )
|| github.event_name == 'pull_request'
name: Test Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
id: buildx
with:
install: true
- name: Available Docker Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Build Debian Packages
run: make debian-packages
- name: List generated files
run: ls -l *.deb
build-packages-manually:
if: github.event_name == 'workflow_dispatch'
name: Build Packages Manually
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
id: buildx
with:
install: true
- name: Available Docker Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Build Debian Packages
run: make debian-packages
- name: Upload package
uses: actions/upload-artifact@v7
with:
name: packages
path: "*.deb"
if-no-files-found: error
retention-days: 3
publish-packages:
if: github.event_name == 'push'
name: Publish Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
id: buildx
with:
install: true
- name: Available Docker Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Build Debian Packages
run: make debian-packages
- name: List generated files
run: ls -l *.deb
- name: Upload packages to repository
env:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
run: for f in *.deb; do curl -F package=@$f https://$FURY_TOKEN@push.fury.io/miniflux/; done
================================================
FILE: .github/workflows/docker.yml
================================================
name: Docker
on:
schedule:
- cron: '0 1 * * *'
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches: [ main ]
paths:
- 'packaging/docker/**'
jobs:
docker-images:
name: Docker Images
if: github.repository_owner == 'miniflux'
permissions:
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Generate Alpine Docker tags
id: docker_alpine_tags
uses: docker/metadata-action@v6
with:
images: |
docker.io/${{ github.repository_owner }}/miniflux
ghcr.io/${{ github.repository_owner }}/miniflux
quay.io/${{ github.repository_owner }}/miniflux
tags: |
type=ref,event=pr
type=schedule,pattern=nightly
type=semver,pattern={{raw}}
- name: Generate Distroless Docker tags
id: docker_distroless_tags
uses: docker/metadata-action@v6
with:
images: |
docker.io/${{ github.repository_owner }}/miniflux
ghcr.io/${{ github.repository_owner }}/miniflux
quay.io/${{ github.repository_owner }}/miniflux
tags: |
type=ref,event=pr
type=schedule,pattern=nightly
type=semver,pattern={{raw}}
flavor: |
suffix=-distroless,onlatest=true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Quay Container Registry
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
uses: docker/login-action@v4
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}
- name: Build and Push Alpine images
uses: docker/build-push-action@v7
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
with:
context: .
file: ./packaging/docker/alpine/Dockerfile
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_alpine_tags.outputs.tags }}
- name: Build and Push Distroless images
uses: docker/build-push-action@v7
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
with:
context: .
file: ./packaging/docker/distroless/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_distroless_tags.outputs.tags }}
================================================
FILE: .github/workflows/linters.yml
================================================
name: Linters
permissions: read-all
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
jshint:
name: Javascript Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install linters
run: |
sudo npm install -g jshint@2.13.6 eslint@8.57.0
- name: Run jshint
run: jshint internal/ui/static/js/*.js
- name: Run ESLint
run: eslint internal/ui/static/js/*.js
golangci:
name: Golang Linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: stable
- uses: golangci/golangci-lint-action@v9
- name: Run gofmt linter
run: gofmt -d -e .
commitlint:
if: github.event_name == 'pull_request'
name: Commit Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Validate PR commits
run: python3 .github/workflows/scripts/commit-checker.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
================================================
FILE: .github/workflows/rpm_packages.yml
================================================
name: RPM Packages
permissions: read-all
on:
workflow_dispatch:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
schedule:
- cron: '0 0 * * 1,4' # Runs at 00:00 UTC on Monday and Thursday
pull_request:
branches: [ main ]
paths:
- 'packaging/rpm/**' # Only run on changes to the rpm packaging files
- '.github/workflows/rpm_packages.yml'
jobs:
test-package:
if: github.event_name == 'schedule' || github.event_name == 'pull_request'
name: Test Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build RPM Package
run: make rpm VERSION=2.2.x_dev
- name: List generated files
run: ls -l *.rpm
build-package-manually:
if: github.event_name == 'workflow_dispatch'
name: Build Packages Manually
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build RPM Package
run: make rpm
- name: Upload package
uses: actions/upload-artifact@v7
with:
name: packages
path: "*.rpm"
if-no-files-found: error
retention-days: 3
publish-package:
if: github.event_name == 'push'
name: Publish Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build RPM Package
run: make rpm
- name: List generated files
run: ls -l *.rpm
- name: Upload package to repository
env:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
run: for f in *.rpm; do curl -F package=@$f https://$FURY_TOKEN@push.fury.io/miniflux/; done
================================================
FILE: .github/workflows/scripts/commit-checker.py
================================================
import subprocess
import re
import sys
import argparse
from typing import Match
# Conventional commit pattern (including Git revert messages)
CONVENTIONAL_COMMIT_PATTERN: str = (
r"^((build|chore|ci|docs|feat|fix|perf|refactor|revert|security|style|test)(\([a-z0-9-]+\))?!?: .{1,100}|Revert .+)"
)
def get_commit_message(commit_hash: str) -> str:
"""Get the commit message for a given commit hash."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "show", "-s", "--format=%B", commit_hash],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error retrieving commit message: {e}")
sys.exit(1)
def check_commit_message(message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN) -> bool:
"""Check if commit message follows conventional commit format."""
first_line: str = message.split("\n")[0]
match: Match[str] | None = re.match(pattern, first_line)
return bool(match)
def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
"""Check all commits in a range for compliance."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
capture_output=True,
text=True,
check=True,
)
commit_hashes: list[str] = result.stdout.strip().split("\n")
# Filter out empty lines
commit_hashes = [hash for hash in commit_hashes if hash]
non_compliant: list[dict[str, str]] = []
for commit_hash in commit_hashes:
message: str = get_commit_message(commit_hash)
if not check_commit_message(message):
non_compliant.append({"hash": commit_hash, "message": message.split("\n")[0]})
return non_compliant
except subprocess.CalledProcessError as e:
print(f"Error checking commit range: {e}")
sys.exit(1)
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Check conventional commit compliance")
parser.add_argument("--base", required=True, help="Base ref (starting commit, exclusive)")
parser.add_argument("--head", required=True, help="Head ref (ending commit, inclusive)")
args: argparse.Namespace = parser.parse_args()
non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
if non_compliant:
print("The following commits do not follow the conventional commit format:")
for commit in non_compliant:
print(f"- {commit['hash'][:8]}: {commit['message']}")
print("\nPlease ensure your commit messages follow the format:")
print("type(scope): subject")
print("\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test")
sys.exit(1)
else:
print("All commits follow the conventional commit format!")
sys.exit(0)
if __name__ == "__main__":
main()
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
permissions: read-all
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
unit-tests:
name: Unit Tests
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests with coverage and race conditions checking
if: matrix.os == 'ubuntu-latest'
run: make test
- name: Run unit tests without coverage and race conditions checking
if: matrix.os != 'ubuntu-latest'
run: go test ./...
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:9.5
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Install Postgres client
run: sudo apt update && sudo apt install -y postgresql-client
- name: Run integration tests
run: make integration-test
env:
PGHOST: 127.0.0.1
PGPASSWORD: postgres
================================================
FILE: .gitignore
================================================
./*.sha256
/miniflux
.idea
.vscode
*.deb
*.rpm
miniflux-*
================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
default: standard
disable:
- errcheck
enable:
- errname
- gocritic
- goheader
- loggercheck
- misspell
- perfsprint
- sqlclosecheck
- staticcheck
- whitespace
settings:
loggercheck:
slog: true
goheader:
template: |-
SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
SPDX-License-Identifier: Apache-2.0
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Miniflux
This document outlines how to contribute effectively to Miniflux.
## Philosophy
Miniflux follows a **minimalist philosophy**. The feature set is intentionally kept limited to avoid bloatware. Before contributing, please understand that:
- **Improving existing features takes priority over adding new ones**
- **Quality over quantity** - well-implemented, focused features are preferred
- **Simplicity is key** - complex solutions are discouraged in favor of simple, maintainable code
## Before You Start
### Feature Requests
Before implementing a new feature:
- Check if it aligns with Miniflux's philosophy
- Consider if the feature could be implemented differently to maintain simplicity
- Remember that developing software takes significant time, and this is a volunteer-driven project
- If you need a specific feature, the best approach is to contribute it yourself
### Bug Reports
When reporting bugs:
- Search existing issues first to avoid duplicates
- Provide clear reproduction steps
- Include relevant system information (OS, browser, Miniflux version)
- Include error messages, screenshots, and logs when applicable
## Development Setup
### Requirements
- **Git**
- **Go >= 1.24**
- **PostgreSQL**
### Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork locally:**
```bash
git clone https://github.com/YOUR_USERNAME/miniflux.git
cd miniflux
```
3. **Build the application binary:**
```bash
make miniflux
```
4. **Run locally in debug mode:**
```bash
make run
```
### Database Setup
For development and testing, you can run a local PostgreSQL database with Docker:
```bash
# Start PostgreSQL container
docker run --rm --name miniflux2-db -p 5432:5432 \
-e POSTGRES_DB=miniflux2 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
postgres
```
You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE_URL` environment variable accordingly.
## Development Workflow
### Code Quality
1. **Run the linter:**
```bash
make lint
```
Requires `staticcheck` and `golangci-lint` to be installed.
2. **Run unit tests:**
```bash
make test
```
3. **Run integration tests:**
```bash
make integration-test
make clean-integration-test
```
### Building
- **Current platform:** `make miniflux`
- **All platforms:** `make build`
- **Specific platforms:** `make linux-amd64`, `make darwin-arm64`, etc.
- **Docker image:** `make docker-image`
### Cross-Platform Support
Miniflux supports multiple architectures. When making changes, ensure compatibility across:
- Linux (amd64, arm64, armv7, armv6, armv5)
- macOS (amd64, arm64)
- FreeBSD, OpenBSD, Windows (amd64)
## Pull Request Guidelines
### What Is Preferred
✅ **Good Pull Requests:**
- Focus on a single issue or feature
- Include tests for new functionality
- Maintain or improve performance
- Follow existing code style and patterns
- The commit messages follow the [conventional commit format](https://www.conventionalcommits.org/) (e.g., `feat: add new feature`, `fix: resolve bug`)
- Update documentation when necessary
### What to Avoid
❌ **Pull Requests That Cannot Be Accepted:**
- **Too many changes** - makes review difficult
- **Breaking changes** - disrupts existing functionality
- **New bugs or regressions** - reduces software quality
- **Unnecessary dependencies** - conflicts with minimalist approach
- **Performance degradation** - slows down the software
- **Poor-quality code** - hard to maintain
- **Dependent PRs** - creates review complexity
- **Radical UI changes** - disrupts user experience
- **Conflicts with philosophy** - doesn't align with minimalist approach
### Pull Request Template
When creating a pull request, please include:
- **Description:** What does this PR do?
- **Motivation:** Why is this change needed?
- **Testing:** How was this tested?
- **Breaking Changes:** Are there any breaking changes?
- **Related Issues:** Link to any related issues
## Code Style
- Follow Go conventions and best practices
- Use `gofmt` to format your Go code, and `jshint` for JavaScript
- Write clear, descriptive variable and function names
- Include comments for complex logic
- Keep functions small and focused
## Testing
### Unit Tests
- Write unit tests for new functions and methods
- Ensure tests are fast and don't require external dependencies
- Aim for good test coverage
### Integration Tests
- Add integration tests for new API endpoints
- Tests run against a real PostgreSQL database
- Ensure tests clean up after themselves
## Communication
- **Discussions:** Use GitHub Discussions for general questions and community interaction
- **Issues:** Use GitHub issues for bug reports and feature requests
- **Pull Requests:** Use PR comments for code-specific discussions
- **Philosophy Questions:** Refer to the FAQ for common questions about project direction
## Questions?
- Check the [FAQ](https://miniflux.app/faq.html) for common questions
- Review the [development documentation](https://miniflux.app/docs/development.html) and [internationalization guide](https://miniflux.app/docs/i18n.html)
- Look at existing issues and pull requests for examples
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: Makefile
================================================
APP := miniflux
DOCKER_IMAGE := miniflux/miniflux
VERSION := $(shell git describe --tags --exact-match 2>/dev/null)
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)'"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
DOCKER_PLATFORM := amd64
export PGPASSWORD := postgres
.PHONY: \
miniflux \
miniflux-no-pie \
linux-amd64 \
linux-arm64 \
linux-armv7 \
linux-armv6 \
linux-armv5 \
darwin-amd64 \
darwin-arm64 \
freebsd-amd64 \
openbsd-amd64 \
build \
run \
clean \
add-string \
test \
lint \
integration-test \
clean-integration-test \
docker-image \
docker-image-distroless \
docker-images \
rpm \
debian \
debian-packages
miniflux:
@ go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP)
miniflux-no-pie:
@ go build -ldflags=$(LD_FLAGS) -o $(APP)
linux-amd64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-arm64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv7:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv6:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv5:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
darwin-amd64:
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
darwin-arm64:
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
freebsd-amd64:
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
openbsd-amd64:
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64
run:
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
clean:
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
add-string:
cd internal/locale/translations && \
for file in *.json; do \
jq --indent 4 --arg key "$(KEY)" --arg val "$(VAL)" \
'. + {($$key): $$val} | to_entries | sort_by(.key) | from_entries' "$$file" > tmp && \
mv tmp "$$file"; \
done
test:
go test -cover -race -count=1 ./...
lint:
go vet ./...
test -z "$$(gofmt -l .)"
golangci-lint run
integration-test:
psql -U postgres -c 'drop database if exists miniflux_test;'
psql -U postgres -c 'create database miniflux_test;'
DATABASE_URL=$(DB_URL) \
ADMIN_USERNAME=admin \
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
RUN_MIGRATIONS=1 \
LOG_LEVEL=debug \
FETCHER_ALLOW_PRIVATE_NETWORKS=1 \
INTEGRATION_ALLOW_PRIVATE_NETWORKS=1 \
go run main.go >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! nc -z localhost 8080; do sleep 1; done
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
TEST_MINIFLUX_ADMIN_USERNAME=admin \
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
go test -v -count=1 ./internal/api
clean-integration-test:
@ kill -9 `cat /tmp/miniflux.pid`
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
@ psql -U postgres -c 'drop database if exists miniflux_test;'
docker-image:
docker build --pull -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/alpine/Dockerfile .
docker-image-distroless:
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/distroless/Dockerfile .
docker-images:
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
--file packaging/docker/alpine/Dockerfile \
--tag $(DOCKER_IMAGE):$(VERSION) \
--push .
rpm: clean
@ docker build \
-t miniflux-rpm-builder \
-f packaging/rpm/Dockerfile \
.
@ docker run --rm \
-v ${PWD}:/root/rpmbuild/RPMS/x86_64 miniflux-rpm-builder \
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
debian:
@ docker buildx build --load \
--platform linux/$(DOCKER_PLATFORM) \
-t miniflux-deb-builder \
-f packaging/debian/Dockerfile \
.
@ docker run --rm --platform linux/$(DOCKER_PLATFORM) \
-v ${PWD}:/pkg miniflux-deb-builder
debian-packages: clean
$(MAKE) debian DOCKER_PLATFORM=amd64
$(MAKE) debian DOCKER_PLATFORM=arm64
$(MAKE) debian DOCKER_PLATFORM=arm/v7
================================================
FILE: Procfile
================================================
web: miniflux.app
================================================
FILE: README.md
================================================
Miniflux 2
==========
Miniflux is a minimalist and opinionated feed reader.
It's simple, fast, lightweight and super easy to install.
Official website: <https://miniflux.app>
Features
--------
### Feed Reader
- Supported feed formats: Atom 0.3/1.0, RSS 1.0/2.0, and JSON Feed 1.0/1.1.
- [OPML](https://en.wikipedia.org/wiki/OPML) file import/export and URL import.
- Supports multiple attachments (podcasts, videos, music, and images enclosures).
- Plays videos from YouTube directly inside Miniflux.
- Organizes articles using categories and bookmarks.
- Share individual articles publicly.
- Fetches website icons (favicons).
- Saves articles to third-party services.
- Provides full-text search (powered by Postgres).
- Available in 20 languages: Portuguese (Brazilian), Chinese (Simplified and Traditional), Dutch, English (US), Finnish, French, German, Greek, Hindi, Indonesian, Italian, Japanese, Polish, Romanian, Russian, Taiwanese POJ, Ukrainian, Spanish, and Turkish.
### Privacy and Security
- Removes pixel trackers.
- Strips tracking parameters from URLs (e.g., `utm_source`, `utm_medium`, `utm_campaign`, `fbclid`, etc.).
- Retrieves original links when feeds are sourced from FeedBurner.
- Opens external links with attributes `rel="noopener noreferrer" referrerpolicy="no-referrer"` for improved security.
- Implements the HTTP header `Referrer-Policy: no-referrer` to prevent referrer leakage.
- Provides a media proxy to avoid tracking and resolve mixed content warnings when using HTTPS.
- Plays YouTube videos via the privacy-focused domain `youtube-nocookie.com`.
- Supports alternative YouTube video players such as [Invidious](https://invidio.us).
- Blocks external JavaScript to prevent tracking and enhance security.
- Sanitizes external content before rendering it.
- Enforces a [Content Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) and a [Trusted Types Policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to only application JavaScript and blocks inline scripts and styles.
### Bot Protection Bypass Mechanisms
- Optionally disable HTTP/2 to mitigate fingerprinting.
- Allows configuration of a custom user agent.
- Supports adding custom cookies for specific use cases.
- Enables the use of proxies for enhanced privacy or bypassing restrictions.
### Content Manipulation
- Fetches the original article and extracts only the relevant content using a local Readability parser.
- Allows custom scraper rules based on <abbr title="Cascading Style Sheets">CSS</abbr> selectors.
- Supports custom rewriting rules for content manipulation.
- Provides a regex filter to include or exclude articles based on specific patterns.
- Optionally permits self-signed or invalid certificates (disabled by default).
- Scrapes YouTube's website to retrieve video duration as read time or uses the YouTube API (disabled by default).
### User Interface
- Optimized stylesheet for readability.
- Responsive design that adapts seamlessly to desktop, tablet, and mobile devices.
- Minimalistic and distraction-free user interface.
- No requirement to download an app from Apple App Store or Google Play Store.
- Can be added directly to the home screen for quick access.
- Supports a wide range of keyboard shortcuts for efficient navigation.
- Optional touch gesture support for navigation on mobile devices.
- Custom stylesheets and JavaScript to personalize the user interface to your preferences.
- Themes:
- Light (Sans-Serif)
- Light (Serif)
- Dark (Sans-Serif)
- Dark (Serif)
- System (Sans-Serif) – Automatically switches between Dark and Light themes based on system preferences.
- System (Serif)
### Integrations
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkTaco](https://linktaco.com), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
- Bookmarklet for subscribing to websites directly from any web browser.
- Webhooks for real-time notifications or custom integrations.
- Compatibility with existing mobile applications using the Fever or Google Reader API.
- REST API with client libraries available in [Go](https://github.com/miniflux/v2/tree/main/client) and [Python](https://github.com/miniflux/python-client).
### Authentication
- Local username and password.
- Passkeys ([WebAuthn](https://en.wikipedia.org/wiki/WebAuthn)).
- Google (OAuth2).
- Generic OpenID Connect.
- Reverse-Proxy authentication.
### Technical Stuff
- Written in [Go (Golang)](https://golang.org/).
- Single binary compiled statically without dependency.
- Works only with [PostgreSQL](https://www.postgresql.org/).
- Does not use any ORM or any complicated frameworks.
- Uses modern vanilla JavaScript only when necessary.
- All static files are bundled into the application binary using the Go `embed` package.
- Supports the Systemd `sd_notify` protocol for process monitoring.
- Configures HTTPS automatically with Let's Encrypt.
- Allows the use of custom <abbr title="Secure Sockets Layer">SSL</abbr> certificates.
- Supports [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2) when TLS is enabled.
- Updates feeds in the background using an internal scheduler or a traditional cron job.
- Uses native lazy loading for images and iframes.
- Compatible only with modern browsers.
- Adheres to the [Twelve-Factor App](https://12factor.net/) methodology.
- Provides official Debian/RPM packages and pre-built binaries.
- Publishes a Docker image to Docker Hub, GitHub Registry, and Quay.io Registry, with ARM architecture support.
- Uses a limited amount of third-party go dependencies
- Has a comprehensive testsuite, with both unit tests and integration tests.
- Only uses a couple of MB of memory and a negligible amount of CPU, even with several hundreds of feeds.
- Respects/sends Last-Modified, If-Modified-Since, If-None-Match, Cache-Control, Expires and ETags headers, and has a default polling interval of 1h.
Documentation
-------------
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
- [Opinionated?](https://miniflux.app/opinionated.html)
- [Features](https://miniflux.app/features.html)
- [Requirements](https://miniflux.app/docs/requirements.html)
- [Installation Instructions](https://miniflux.app/docs/installation.html)
- [Upgrading to a New Version](https://miniflux.app/docs/upgrade.html)
- [Configuration](https://miniflux.app/docs/configuration.html)
- [Command Line Usage](https://miniflux.app/docs/cli.html)
- [User Interface Usage](https://miniflux.app/docs/ui.html)
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
- [Integration with External Services](https://miniflux.app/docs/#integrations)
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
- [API Reference](https://miniflux.app/docs/api.html)
- [Development](https://miniflux.app/docs/development.html)
- [Internationalization](https://miniflux.app/docs/i18n.html)
- [Frequently Asked Questions](https://miniflux.app/faq.html)
Screenshots
-----------
Default theme:

Dark theme when using keyboard navigation:

Credits
-------
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
- Distributed under Apache 2.0 License
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
Only the latest stable version is supported.
## Reporting a Vulnerability
Preferably, [report the vulnerability privately using GitHub](https://github.com/miniflux/v2/security/advisories/new) ([documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)).
If you do not want to use GitHub, send an email to `security AT miniflux DOT net` with all the steps to reproduce the problem.
================================================
FILE: client/README.md
================================================
Miniflux API Client
===================
[](https://pkg.go.dev/miniflux.app/v2/client)
Go client for the Miniflux REST API. It supports API tokens or basic authentication and mirrors the server endpoints closely.
Installation
------------
```bash
go get -u miniflux.app/v2/client
```
Example
-------
```go
package main
import (
"fmt"
"os"
miniflux "miniflux.app/v2/client"
)
func main() {
// Authentication with username/password:
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
// Authentication with an API Key:
client := miniflux.NewClient("https://api.example.org", "my-secret-token")
// Fetch all feeds.
feeds, err := client.Feeds()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(feeds)
// Backup your feeds to an OPML file.
opml, err := client.Export()
if err != nil {
fmt.Println(err)
return
}
err = os.WriteFile("opml.xml", opml, 0644)
if err != nil {
fmt.Println(err)
return
}
}
```
================================================
FILE: client/client.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// Client holds API procedure calls.
type Client struct {
request *request
}
// New returns a new Miniflux client.
//
// Deprecated: use NewClient instead.
//
//go:fix inline
func New(endpoint string, credentials ...string) *Client {
return NewClient(endpoint, credentials...)
}
// NewClient returns a new Miniflux client.
func NewClient(endpoint string, credentials ...string) *Client {
switch len(credentials) {
case 2:
return NewClientWithOptions(endpoint, WithCredentials(credentials[0], credentials[1]))
case 1:
return NewClientWithOptions(endpoint, WithAPIKey(credentials[0]))
default:
return NewClientWithOptions(endpoint)
}
}
// NewClientWithOptions returns a new Miniflux client with options.
func NewClientWithOptions(endpoint string, options ...Option) *Client {
// Trim trailing slashes and /v1 from the endpoint.
endpoint = strings.TrimSuffix(endpoint, "/")
endpoint = strings.TrimSuffix(endpoint, "/v1")
request := &request{endpoint: endpoint, client: http.DefaultClient}
for _, option := range options {
option(request)
}
return &Client{request: request}
}
func withDefaultTimeout() (context.Context, func()) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
return ctx, cancel
}
// Healthcheck checks if the application is up and running.
func (c *Client) Healthcheck() error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.HealthcheckContext(ctx)
}
// HealthcheckContext checks if the application is up and running.
func (c *Client) HealthcheckContext(ctx context.Context) error {
body, err := c.request.Get(ctx, "/healthcheck")
if err != nil {
return fmt.Errorf("miniflux: unable to perform healthcheck: %w", err)
}
defer body.Close()
responseBodyContent, err := io.ReadAll(body)
if err != nil {
return fmt.Errorf("miniflux: unable to read healthcheck response: %w", err)
}
if string(responseBodyContent) != "OK" {
return fmt.Errorf("miniflux: invalid healthcheck response: %q", responseBodyContent)
}
return nil
}
// Version returns the version of the Miniflux instance.
func (c *Client) Version() (*VersionResponse, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.VersionContext(ctx)
}
// VersionContext returns the version of the Miniflux instance.
func (c *Client) VersionContext(ctx context.Context) (*VersionResponse, error) {
body, err := c.request.Get(ctx, "/v1/version")
if err != nil {
return nil, err
}
defer body.Close()
var versionResponse *VersionResponse
if err := json.NewDecoder(body).Decode(&versionResponse); err != nil {
return nil, fmt.Errorf("miniflux: json error (%v)", err)
}
return versionResponse, nil
}
// Me returns the logged user information.
func (c *Client) Me() (*User, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.MeContext(ctx)
}
// MeContext returns the logged user information.
func (c *Client) MeContext(ctx context.Context) (*User, error) {
body, err := c.request.Get(ctx, "/v1/me")
if err != nil {
return nil, err
}
defer body.Close()
var user *User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: json error (%v)", err)
}
return user, nil
}
// Users returns all users.
func (c *Client) Users() (Users, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UsersContext(ctx)
}
// UsersContext returns all users.
func (c *Client) UsersContext(ctx context.Context) (Users, error) {
body, err := c.request.Get(ctx, "/v1/users")
if err != nil {
return nil, err
}
defer body.Close()
var users Users
if err := json.NewDecoder(body).Decode(&users); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return users, nil
}
// UserByID returns a single user.
func (c *Client) UserByID(userID int64) (*User, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UserByIDContext(ctx, userID)
}
// UserByIDContext returns a single user.
func (c *Client) UserByIDContext(ctx context.Context, userID int64) (*User, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/users/%d", userID))
if err != nil {
return nil, err
}
defer body.Close()
var user User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &user, nil
}
// UserByUsername returns a single user.
func (c *Client) UserByUsername(username string) (*User, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UserByUsernameContext(ctx, username)
}
// UserByUsernameContext returns a single user.
func (c *Client) UserByUsernameContext(ctx context.Context, username string) (*User, error) {
body, err := c.request.Get(ctx, "/v1/users/"+username)
if err != nil {
return nil, err
}
defer body.Close()
var user User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &user, nil
}
// CreateUser creates a new user in the system.
func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CreateUserContext(ctx, username, password, isAdmin)
}
// CreateUserContext creates a new user in the system.
func (c *Client) CreateUserContext(ctx context.Context, username, password string, isAdmin bool) (*User, error) {
body, err := c.request.Post(ctx, "/v1/users", &UserCreationRequest{
Username: username,
Password: password,
IsAdmin: isAdmin,
})
if err != nil {
return nil, err
}
defer body.Close()
var user *User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return user, nil
}
// UpdateUser updates a user in the system.
func (c *Client) UpdateUser(userID int64, userChanges *UserModificationRequest) (*User, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateUserContext(ctx, userID, userChanges)
}
// UpdateUserContext updates a user in the system.
func (c *Client) UpdateUserContext(ctx context.Context, userID int64, userChanges *UserModificationRequest) (*User, error) {
body, err := c.request.Put(ctx, fmt.Sprintf("/v1/users/%d", userID), userChanges)
if err != nil {
return nil, err
}
defer body.Close()
var u *User
if err := json.NewDecoder(body).Decode(&u); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return u, nil
}
// DeleteUser removes a user from the system.
func (c *Client) DeleteUser(userID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.DeleteUserContext(ctx, userID)
}
// DeleteUserContext removes a user from the system.
func (c *Client) DeleteUserContext(ctx context.Context, userID int64) error {
return c.request.Delete(ctx, fmt.Sprintf("/v1/users/%d", userID))
}
// APIKeys returns all API keys for the authenticated user.
func (c *Client) APIKeys() (APIKeys, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.APIKeysContext(ctx)
}
// APIKeysContext returns all API keys for the authenticated user.
func (c *Client) APIKeysContext(ctx context.Context) (APIKeys, error) {
body, err := c.request.Get(ctx, "/v1/api-keys")
if err != nil {
return nil, err
}
defer body.Close()
var apiKeys APIKeys
if err := json.NewDecoder(body).Decode(&apiKeys); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return apiKeys, nil
}
// CreateAPIKey creates a new API key for the authenticated user.
func (c *Client) CreateAPIKey(description string) (*APIKey, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CreateAPIKeyContext(ctx, description)
}
// CreateAPIKeyContext creates a new API key for the authenticated user.
func (c *Client) CreateAPIKeyContext(ctx context.Context, description string) (*APIKey, error) {
body, err := c.request.Post(ctx, "/v1/api-keys", &APIKeyCreationRequest{
Description: description,
})
if err != nil {
return nil, err
}
defer body.Close()
var apiKey *APIKey
if err := json.NewDecoder(body).Decode(&apiKey); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return apiKey, nil
}
// DeleteAPIKey removes an API key for the authenticated user.
func (c *Client) DeleteAPIKey(apiKeyID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.DeleteAPIKeyContext(ctx, apiKeyID)
}
// DeleteAPIKeyContext removes an API key for the authenticated user.
func (c *Client) DeleteAPIKeyContext(ctx context.Context, apiKeyID int64) error {
return c.request.Delete(ctx, fmt.Sprintf("/v1/api-keys/%d", apiKeyID))
}
// MarkAllAsRead marks all unread entries as read for a given user.
func (c *Client) MarkAllAsRead(userID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.MarkAllAsReadContext(ctx, userID)
}
// MarkAllAsReadContext marks all unread entries as read for a given user.
func (c *Client) MarkAllAsReadContext(ctx context.Context, userID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
return err
}
// IntegrationsStatus fetches the integrations status for the signed-in user.
func (c *Client) IntegrationsStatus() (bool, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.IntegrationsStatusContext(ctx)
}
// IntegrationsStatusContext fetches the integrations status for the signed-in user.
func (c *Client) IntegrationsStatusContext(ctx context.Context) (bool, error) {
body, err := c.request.Get(ctx, "/v1/integrations/status")
if err != nil {
return false, err
}
defer body.Close()
var response struct {
HasIntegrations bool `json:"has_integrations"`
}
if err := json.NewDecoder(body).Decode(&response); err != nil {
return false, fmt.Errorf("miniflux: response error (%v)", err)
}
return response.HasIntegrations, nil
}
// Discover tries to find subscriptions on a website.
func (c *Client) Discover(url string) (Subscriptions, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.DiscoverContext(ctx, url)
}
// DiscoverContext tries to find subscriptions from a website.
func (c *Client) DiscoverContext(ctx context.Context, url string) (Subscriptions, error) {
body, err := c.request.Post(ctx, "/v1/discover", map[string]string{"url": url})
if err != nil {
return nil, err
}
defer body.Close()
var subscriptions Subscriptions
if err := json.NewDecoder(body).Decode(&subscriptions); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return subscriptions, nil
}
// Categories retrieves the list of categories.
func (c *Client) Categories() (Categories, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CategoriesContext(ctx)
}
// CategoriesContext retrieves the list of categories.
func (c *Client) CategoriesContext(ctx context.Context) (Categories, error) {
body, err := c.request.Get(ctx, "/v1/categories")
if err != nil {
return nil, err
}
defer body.Close()
var categories Categories
if err := json.NewDecoder(body).Decode(&categories); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return categories, nil
}
// CategoriesWithCounters fetches the categories with their respective feed and unread counts.
func (c *Client) CategoriesWithCounters() (Categories, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CategoriesWithCountersContext(ctx)
}
// CategoriesWithCountersContext fetches the categories with their respective feed and unread counts.
func (c *Client) CategoriesWithCountersContext(ctx context.Context) (Categories, error) {
body, err := c.request.Get(ctx, "/v1/categories?counts=true")
if err != nil {
return nil, err
}
defer body.Close()
var categories Categories
if err := json.NewDecoder(body).Decode(&categories); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return categories, nil
}
// CreateCategory creates a new category.
func (c *Client) CreateCategory(title string) (*Category, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CreateCategoryContext(ctx, title)
}
// CreateCategoryContext creates a new category.
func (c *Client) CreateCategoryContext(ctx context.Context, title string) (*Category, error) {
body, err := c.request.Post(ctx, "/v1/categories", &CategoryCreationRequest{
Title: title,
})
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// CreateCategoryWithOptions creates a new category with options.
func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCreationRequest) (*Category, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CreateCategoryWithOptionsContext(ctx, createRequest)
}
// CreateCategoryWithOptionsContext creates a new category with options.
func (c *Client) CreateCategoryWithOptionsContext(ctx context.Context, createRequest *CategoryCreationRequest) (*Category, error) {
body, err := c.request.Post(ctx, "/v1/categories", createRequest)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// UpdateCategory updates a category.
func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateCategoryContext(ctx, categoryID, title)
}
// UpdateCategoryContext updates a category.
func (c *Client) UpdateCategoryContext(ctx context.Context, categoryID int64, title string) (*Category, error) {
body, err := c.request.Put(ctx, fmt.Sprintf("/v1/categories/%d", categoryID), &CategoryModificationRequest{
Title: new(title),
})
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// UpdateCategoryWithOptions updates a category with options.
func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateCategoryWithOptionsContext(ctx, categoryID, categoryChanges)
}
// UpdateCategoryWithOptionsContext updates a category with options.
func (c *Client) UpdateCategoryWithOptionsContext(ctx context.Context, categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
body, err := c.request.Put(ctx, fmt.Sprintf("/v1/categories/%d", categoryID), categoryChanges)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.MarkCategoryAsReadContext(ctx, categoryID)
}
// MarkCategoryAsReadContext marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsReadContext(ctx context.Context, categoryID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
return err
}
// CategoryFeeds returns all feeds for a category.
func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CategoryFeedsContext(ctx, categoryID)
}
// CategoryFeedsContext returns all feeds for a category.
func (c *Client) CategoryFeedsContext(ctx context.Context, categoryID int64) (Feeds, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/categories/%d/feeds", categoryID))
if err != nil {
return nil, err
}
defer body.Close()
var feeds Feeds
if err := json.NewDecoder(body).Decode(&feeds); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feeds, nil
}
// DeleteCategory removes a category.
func (c *Client) DeleteCategory(categoryID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.DeleteCategoryContext(ctx, categoryID)
}
// DeleteCategoryContext removes a category.
func (c *Client) DeleteCategoryContext(ctx context.Context, categoryID int64) error {
return c.request.Delete(ctx, fmt.Sprintf("/v1/categories/%d", categoryID))
}
// RefreshCategory refreshes a category.
func (c *Client) RefreshCategory(categoryID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.RefreshCategoryContext(ctx, categoryID)
}
// RefreshCategoryContext refreshes a category.
func (c *Client) RefreshCategoryContext(ctx context.Context, categoryID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/categories/%d/refresh", categoryID), nil)
return err
}
// Feeds gets all feeds.
func (c *Client) Feeds() (Feeds, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FeedsContext(ctx)
}
// FeedsContext gets all feeds.
func (c *Client) FeedsContext(ctx context.Context) (Feeds, error) {
body, err := c.request.Get(ctx, "/v1/feeds")
if err != nil {
return nil, err
}
defer body.Close()
var feeds Feeds
if err := json.NewDecoder(body).Decode(&feeds); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feeds, nil
}
// Export exports subscriptions as an OPML document.
func (c *Client) Export() ([]byte, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.ExportContext(ctx)
}
// ExportContext exports subscriptions as an OPML document.
func (c *Client) ExportContext(ctx context.Context) ([]byte, error) {
body, err := c.request.Get(ctx, "/v1/export")
if err != nil {
return nil, err
}
defer body.Close()
opml, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return opml, nil
}
// Import imports an OPML file.
func (c *Client) Import(f io.ReadCloser) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.ImportContext(ctx, f)
}
// ImportContext imports an OPML file.
func (c *Client) ImportContext(ctx context.Context, f io.ReadCloser) error {
_, err := c.request.PostFile(ctx, "/v1/import", f)
return err
}
// Feed gets a feed.
func (c *Client) Feed(feedID int64) (*Feed, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FeedContext(ctx, feedID)
}
// FeedContext gets a feed.
func (c *Client) FeedContext(ctx context.Context, feedID int64) (*Feed, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/feeds/%d", feedID))
if err != nil {
return nil, err
}
defer body.Close()
var feed *Feed
if err := json.NewDecoder(body).Decode(&feed); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feed, nil
}
// CreateFeed creates a new feed.
func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) (int64, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CreateFeedContext(ctx, feedCreationRequest)
}
// CreateFeedContext creates a new feed.
func (c *Client) CreateFeedContext(ctx context.Context, feedCreationRequest *FeedCreationRequest) (int64, error) {
body, err := c.request.Post(ctx, "/v1/feeds", feedCreationRequest)
if err != nil {
return 0, err
}
defer body.Close()
type result struct {
FeedID int64 `json:"feed_id"`
}
var r result
if err := json.NewDecoder(body).Decode(&r); err != nil {
return 0, fmt.Errorf("miniflux: response error (%v)", err)
}
return r.FeedID, nil
}
// UpdateFeed updates a feed.
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateFeedContext(ctx, feedID, feedChanges)
}
// UpdateFeedContext updates a feed.
func (c *Client) UpdateFeedContext(ctx context.Context, feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
body, err := c.request.Put(ctx, fmt.Sprintf("/v1/feeds/%d", feedID), feedChanges)
if err != nil {
return nil, err
}
defer body.Close()
var f *Feed
if err := json.NewDecoder(body).Decode(&f); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return f, nil
}
// ImportFeedEntry imports a single entry into a feed.
func (c *Client) ImportFeedEntry(feedID int64, payload any) (int64, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
body, err := c.request.Post(
ctx,
fmt.Sprintf("/v1/feeds/%d/entries/import", feedID),
payload,
)
if err != nil {
return 0, err
}
defer body.Close()
var response struct {
ID int64 `json:"id"`
}
if err := json.NewDecoder(body).Decode(&response); err != nil {
return 0, fmt.Errorf("miniflux: json error (%v)", err)
}
return response.ID, nil
}
// MarkFeedAsRead marks all unread entries of the feed as read.
func (c *Client) MarkFeedAsRead(feedID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.MarkFeedAsReadContext(ctx, feedID)
}
// MarkFeedAsReadContext marks all unread entries of the feed as read.
func (c *Client) MarkFeedAsReadContext(ctx context.Context, feedID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
return err
}
// RefreshAllFeeds refreshes all feeds.
func (c *Client) RefreshAllFeeds() error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.RefreshAllFeedsContext(ctx)
}
// RefreshAllFeedsContext refreshes all feeds.
func (c *Client) RefreshAllFeedsContext(ctx context.Context) error {
_, err := c.request.Put(ctx, "/v1/feeds/refresh", nil)
return err
}
// RefreshFeed refreshes a feed.
func (c *Client) RefreshFeed(feedID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.RefreshFeedContext(ctx, feedID)
}
// RefreshFeedContext refreshes a feed.
func (c *Client) RefreshFeedContext(ctx context.Context, feedID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
return err
}
// DeleteFeed removes a feed.
func (c *Client) DeleteFeed(feedID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.DeleteFeedContext(ctx, feedID)
}
// DeleteFeedContext removes a feed.
func (c *Client) DeleteFeedContext(ctx context.Context, feedID int64) error {
return c.request.Delete(ctx, fmt.Sprintf("/v1/feeds/%d", feedID))
}
// FeedIcon gets a feed icon.
func (c *Client) FeedIcon(feedID int64) (*FeedIcon, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FeedIconContext(ctx, feedID)
}
// FeedIconContext gets a feed icon.
func (c *Client) FeedIconContext(ctx context.Context, feedID int64) (*FeedIcon, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/feeds/%d/icon", feedID))
if err != nil {
return nil, err
}
defer body.Close()
var feedIcon *FeedIcon
if err := json.NewDecoder(body).Decode(&feedIcon); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feedIcon, nil
}
// FeedEntry gets a single feed entry.
func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FeedEntryContext(ctx, feedID, entryID)
}
// FeedEntryContext gets a single feed entry.
func (c *Client) FeedEntryContext(ctx context.Context, feedID, entryID int64) (*Entry, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/feeds/%d/entries/%d", feedID, entryID))
if err != nil {
return nil, err
}
defer body.Close()
var entry *Entry
if err := json.NewDecoder(body).Decode(&entry); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return entry, nil
}
// CategoryEntry gets a single category entry.
func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CategoryEntryContext(ctx, categoryID, entryID)
}
// CategoryEntryContext gets a single category entry.
func (c *Client) CategoryEntryContext(ctx context.Context, categoryID, entryID int64) (*Entry, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID))
if err != nil {
return nil, err
}
defer body.Close()
var entry *Entry
if err := json.NewDecoder(body).Decode(&entry); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return entry, nil
}
// Entry gets a single entry.
func (c *Client) Entry(entryID int64) (*Entry, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.EntryContext(ctx, entryID)
}
// EntryContext gets a single entry.
func (c *Client) EntryContext(ctx context.Context, entryID int64) (*Entry, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/entries/%d", entryID))
if err != nil {
return nil, err
}
defer body.Close()
var entry *Entry
if err := json.NewDecoder(body).Decode(&entry); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return entry, nil
}
// Entries fetches entries using the given filter.
func (c *Client) Entries(filter *Filter) (*EntryResultSet, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.EntriesContext(ctx, filter)
}
// EntriesContext fetches entries.
func (c *Client) EntriesContext(ctx context.Context, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString("/v1/entries", filter)
body, err := c.request.Get(ctx, path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// FeedEntries fetches entries for a feed using the given filter.
func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FeedEntriesContext(ctx, feedID, filter)
}
// FeedEntriesContext fetches feed entries.
func (c *Client) FeedEntriesContext(ctx context.Context, feedID int64, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString(fmt.Sprintf("/v1/feeds/%d/entries", feedID), filter)
body, err := c.request.Get(ctx, path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// CategoryEntries fetches entries for a category using the given filter.
func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.CategoryEntriesContext(ctx, categoryID, filter)
}
// CategoryEntriesContext fetches category entries.
func (c *Client) CategoryEntriesContext(ctx context.Context, categoryID int64, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter)
body, err := c.request.Get(ctx, path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// UpdateEntries updates the status of a list of entries.
func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateEntriesContext(ctx, entryIDs, status)
}
// UpdateEntriesContext updates the status of a list of entries.
func (c *Client) UpdateEntriesContext(ctx context.Context, entryIDs []int64, status string) error {
type payload struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}
_, err := c.request.Put(ctx, "/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
return err
}
// UpdateEntry updates an entry.
func (c *Client) UpdateEntry(entryID int64, entryChanges *EntryModificationRequest) (*Entry, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateEntryContext(ctx, entryID, entryChanges)
}
// UpdateEntryContext updates an entry.
func (c *Client) UpdateEntryContext(ctx context.Context, entryID int64, entryChanges *EntryModificationRequest) (*Entry, error) {
body, err := c.request.Put(ctx, fmt.Sprintf("/v1/entries/%d", entryID), entryChanges)
if err != nil {
return nil, err
}
defer body.Close()
var entry *Entry
if err := json.NewDecoder(body).Decode(&entry); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return entry, nil
}
// ToggleStarred toggles the starred flag of an entry.
func (c *Client) ToggleStarred(entryID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.ToggleStarredContext(ctx, entryID)
}
// ToggleStarredContext toggles entry starred value.
func (c *Client) ToggleStarredContext(ctx context.Context, entryID int64) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/entries/%d/star", entryID), nil)
return err
}
// SaveEntry sends an entry to a third-party service.
func (c *Client) SaveEntry(entryID int64) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.SaveEntryContext(ctx, entryID)
}
// SaveEntryContext sends an entry to a third-party service.
func (c *Client) SaveEntryContext(ctx context.Context, entryID int64) error {
_, err := c.request.Post(ctx, fmt.Sprintf("/v1/entries/%d/save", entryID), nil)
return err
}
// FetchEntryOriginalContent fetches the original content of an entry using the scraper.
func (c *Client) FetchEntryOriginalContent(entryID int64) (string, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FetchEntryOriginalContentContext(ctx, entryID)
}
// FetchEntryOriginalContentContext fetches the original content of an entry using the scraper.
func (c *Client) FetchEntryOriginalContentContext(ctx context.Context, entryID int64) (string, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/entries/%d/fetch-content", entryID))
if err != nil {
return "", err
}
defer body.Close()
var response struct {
Content string `json:"content"`
}
if err := json.NewDecoder(body).Decode(&response); err != nil {
return "", fmt.Errorf("miniflux: response error (%v)", err)
}
return response.Content, nil
}
// FetchCounters fetches feed counters.
func (c *Client) FetchCounters() (*FeedCounters, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FetchCountersContext(ctx)
}
// FetchCountersContext fetches feed counters.
func (c *Client) FetchCountersContext(ctx context.Context) (*FeedCounters, error) {
body, err := c.request.Get(ctx, "/v1/feeds/counters")
if err != nil {
return nil, err
}
defer body.Close()
var result FeedCounters
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// FlushHistory changes all entries with the status "read" to "removed".
func (c *Client) FlushHistory() error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.FlushHistoryContext(ctx)
}
// FlushHistoryContext changes all entries with the status "read" to "removed".
func (c *Client) FlushHistoryContext(ctx context.Context) error {
_, err := c.request.Put(ctx, "/v1/flush-history", nil)
return err
}
// Icon fetches a feed icon.
func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.IconContext(ctx, iconID)
}
// IconContext fetches a feed icon.
func (c *Client) IconContext(ctx context.Context, iconID int64) (*FeedIcon, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/icons/%d", iconID))
if err != nil {
return nil, err
}
defer body.Close()
var feedIcon *FeedIcon
if err := json.NewDecoder(body).Decode(&feedIcon); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feedIcon, nil
}
// Enclosure fetches a specific enclosure.
func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.EnclosureContext(ctx, enclosureID)
}
// EnclosureContext fetches a specific enclosure.
func (c *Client) EnclosureContext(ctx context.Context, enclosureID int64) (*Enclosure, error) {
body, err := c.request.Get(ctx, fmt.Sprintf("/v1/enclosures/%d", enclosureID))
if err != nil {
return nil, err
}
defer body.Close()
var enclosure *Enclosure
if err := json.NewDecoder(body).Decode(&enclosure); err != nil {
return nil, fmt.Errorf("miniflux: response error(%v)", err)
}
return enclosure, nil
}
// UpdateEnclosure updates an enclosure.
func (c *Client) UpdateEnclosure(enclosureID int64, enclosureUpdate *EnclosureUpdateRequest) error {
ctx, cancel := withDefaultTimeout()
defer cancel()
return c.UpdateEnclosureContext(ctx, enclosureID, enclosureUpdate)
}
// UpdateEnclosureContext updates an enclosure.
func (c *Client) UpdateEnclosureContext(ctx context.Context, enclosureID int64, enclosureUpdate *EnclosureUpdateRequest) error {
_, err := c.request.Put(ctx, fmt.Sprintf("/v1/enclosures/%d", enclosureID), enclosureUpdate)
return err
}
func buildFilterQueryString(path string, filter *Filter) string {
if filter != nil {
values := url.Values{}
if filter.Status != "" {
values.Set("status", filter.Status)
}
if filter.Direction != "" {
values.Set("direction", filter.Direction)
}
if filter.Order != "" {
values.Set("order", filter.Order)
}
if filter.Limit >= 0 {
values.Set("limit", strconv.Itoa(filter.Limit))
}
if filter.Offset >= 0 {
values.Set("offset", strconv.Itoa(filter.Offset))
}
if filter.After > 0 {
values.Set("after", strconv.FormatInt(filter.After, 10))
}
if filter.Before > 0 {
values.Set("before", strconv.FormatInt(filter.Before, 10))
}
if filter.PublishedAfter > 0 {
values.Set("published_after", strconv.FormatInt(filter.PublishedAfter, 10))
}
if filter.PublishedBefore > 0 {
values.Set("published_before", strconv.FormatInt(filter.PublishedBefore, 10))
}
if filter.ChangedAfter > 0 {
values.Set("changed_after", strconv.FormatInt(filter.ChangedAfter, 10))
}
if filter.ChangedBefore > 0 {
values.Set("changed_before", strconv.FormatInt(filter.ChangedBefore, 10))
}
if filter.AfterEntryID > 0 {
values.Set("after_entry_id", strconv.FormatInt(filter.AfterEntryID, 10))
}
if filter.BeforeEntryID > 0 {
values.Set("before_entry_id", strconv.FormatInt(filter.BeforeEntryID, 10))
}
if filter.Starred != "" {
values.Set("starred", filter.Starred)
}
if filter.Search != "" {
values.Set("search", filter.Search)
}
if filter.CategoryID > 0 {
values.Set("category_id", strconv.FormatInt(filter.CategoryID, 10))
}
if filter.FeedID > 0 {
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
}
if filter.GloballyVisible {
values.Set("globally_visible", "true")
}
for _, status := range filter.Statuses {
values.Add("status", status)
}
path = fmt.Sprintf("%s?%s", path, values.Encode())
}
return path
}
================================================
FILE: client/client_test.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client
import (
"bytes"
"encoding/json"
"io"
"net/http"
"reflect"
"testing"
"time"
)
type roundTripperFunc func(req *http.Request) (*http.Response, error)
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
func newFakeHTTPClient(
t *testing.T,
fn func(t *testing.T, req *http.Request) *http.Response,
) *http.Client {
return &http.Client{
Transport: roundTripperFunc(
func(req *http.Request) (*http.Response, error) {
return fn(t, req), nil
}),
}
}
func jsonResponseFrom(
t *testing.T,
status int,
headers http.Header,
body any,
) *http.Response {
data, err := json.Marshal(body)
if err != nil {
t.Fatalf("Unable to marshal body: %v", err)
}
return &http.Response{
StatusCode: status,
Body: io.NopCloser(bytes.NewBuffer(data)),
Header: headers,
}
}
func asJSON(data any) string {
json, err := json.MarshalIndent(data, "", " ")
if err != nil {
panic(err)
}
return string(json)
}
func expectRequest(
t *testing.T,
method string,
url string,
checkBody func(r io.Reader),
req *http.Request,
) {
if req.Method != method {
t.Fatalf("Expected method to be %s, got %s", method, req.Method)
}
if req.URL.String() != url {
t.Fatalf("Expected URL path to be %s, got %s", url, req.URL)
}
if checkBody != nil {
checkBody(req.Body)
}
}
func expectFromJSON[T any](
t *testing.T,
r io.Reader,
expected *T,
) {
var got T
if err := json.NewDecoder(r).Decode(&got); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(&got, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(got))
}
}
func TestHealthcheck(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/healthcheck", nil, req)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString("OK")),
}
})))
if err := client.HealthcheckContext(t.Context()); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestVersion(t *testing.T) {
expected := &VersionResponse{
Version: "1.0.0",
Commit: "1234567890",
BuildDate: "2021-01-01T00:00:00Z",
GoVersion: "go1.20",
Compiler: "gc",
Arch: "amd64",
OS: "linux",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/version", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.VersionContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestMe(t *testing.T) {
expected := &User{
ID: 1,
Username: "test",
Password: "password",
IsAdmin: false,
Theme: "light",
Language: "en",
Timezone: "UTC",
EntryDirection: "asc",
EntryOrder: "created_at",
Stylesheet: "default",
CustomJS: "custom.js",
GoogleID: "google-id",
OpenIDConnectID: "openid-connect-id",
EntriesPerPage: 10,
KeyboardShortcuts: true,
ShowReadingTime: true,
EntrySwipe: true,
GestureNav: "horizontal",
DisplayMode: "read",
DefaultReadingSpeed: 1,
CJKReadingSpeed: 1,
DefaultHomePage: "home",
CategoriesSortingOrder: "asc",
MarkReadOnView: true,
MediaPlaybackRate: 1.0,
BlockFilterEntryRules: "block",
KeepFilterEntryRules: "keep",
ExternalFontHosts: "https://fonts.googleapis.com",
AlwaysOpenExternalLinks: true,
OpenExternalLinksInNewTab: true,
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/me", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.MeContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUsers(t *testing.T) {
expected := Users{
{
ID: 1,
Username: "test1",
},
{
ID: 2,
Username: "test2",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/users", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UsersContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUserByID(t *testing.T) {
expected := &User{
ID: 1,
Username: "test",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/users/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UserByIDContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUserByUsername(t *testing.T) {
expected := &User{
ID: 1,
Username: "test",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/users/test", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UserByUsernameContext(t.Context(), "test")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCreateUser(t *testing.T) {
expected := &User{
ID: 1,
Username: "test",
Password: "password",
IsAdmin: true,
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
exp := UserCreationRequest{
Username: "test",
Password: "password",
IsAdmin: true,
}
expectRequest(
t,
http.MethodPost,
"http://mf/v1/users",
func(r io.Reader) {
expectFromJSON(t, r, &exp)
},
req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CreateUserContext(t.Context(), "test", "password", true)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUpdateUser(t *testing.T) {
expected := &User{
ID: 1,
Username: "test",
Password: "password",
IsAdmin: true,
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/users/1", func(r io.Reader) {
expectFromJSON(t, r, &UserModificationRequest{
Username: &expected.Username,
Password: &expected.Password,
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UpdateUserContext(t.Context(), 1, &UserModificationRequest{
Username: &expected.Username,
Password: &expected.Password,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestDeleteUser(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodDelete, "http://mf/v1/users/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.DeleteUserContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestAPIKeys(t *testing.T) {
expected := APIKeys{
{
ID: 1,
Token: "token",
Description: "test",
},
{
ID: 2,
Token: "token2",
Description: "test2",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/api-keys", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.APIKeysContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCreateAPIKey(t *testing.T) {
expected := &APIKey{
ID: 42,
Token: "some-token",
Description: "desc",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/api-keys", func(r io.Reader) {
expectFromJSON(t, r, &APIKeyCreationRequest{
Description: "desc",
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CreateAPIKeyContext(t.Context(), "desc")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestDeleteAPIKey(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodDelete, "http://mf/v1/api-keys/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.DeleteAPIKeyContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestMarkAllAsRead(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/users/1/mark-all-as-read", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.MarkAllAsReadContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestIntegrationsStatus(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/integrations/status", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, struct {
HasIntegrations bool `json:"has_integrations"`
}{
HasIntegrations: true,
})
})))
status, err := client.IntegrationsStatusContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !status {
t.Fatalf("Expected integrations status to be true, got false")
}
}
func TestDiscover(t *testing.T) {
expected := Subscriptions{
{
URL: "http://example.com",
Title: "Example",
Type: "rss",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/discover", func(r io.Reader) {
expectFromJSON(t, r, &map[string]string{"url": "http://example.com"})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.DiscoverContext(t.Context(), "http://example.com")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCategories(t *testing.T) {
expected := Categories{
{
ID: 1,
Title: "Example",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/categories", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CategoriesContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCategoriesWithCounters(t *testing.T) {
feedCount := 1
totalUnread := 2
expected := Categories{
{
ID: 1,
Title: "Example",
FeedCount: &feedCount,
TotalUnread: &totalUnread,
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/categories?counts=true", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CategoriesWithCountersContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCreateCategory(t *testing.T) {
expected := &Category{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/categories", func(r io.Reader) {
expectFromJSON(t, r, &CategoryCreationRequest{
Title: "Example",
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CreateCategoryContext(t.Context(), "Example")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestCreateCategoryWithOptions(t *testing.T) {
expected := &Category{
ID: 1,
Title: "Example",
HideGlobally: true,
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/categories", func(r io.Reader) {
expectFromJSON(t, r, &CategoryCreationRequest{
Title: "Example",
HideGlobally: true,
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CreateCategoryWithOptionsContext(t.Context(), &CategoryCreationRequest{
Title: "Example",
HideGlobally: true,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUpdateCategory(t *testing.T) {
expected := &Category{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/categories/1", func(r io.Reader) {
expectFromJSON(t, r, &CategoryModificationRequest{
Title: &expected.Title,
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UpdateCategoryContext(t.Context(), 1, "Example")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestUpdateCategoryWithOptions(t *testing.T) {
expected := &Category{
ID: 1,
Title: "Example",
HideGlobally: true,
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/categories/1", func(r io.Reader) {
expectFromJSON(t, r, &CategoryModificationRequest{
Title: &expected.Title,
HideGlobally: &expected.HideGlobally,
})
}, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UpdateCategoryWithOptionsContext(t.Context(), 1, &CategoryModificationRequest{
Title: &expected.Title,
HideGlobally: &expected.HideGlobally,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestMarkCategoryAsRead(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/categories/1/mark-all-as-read", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.MarkCategoryAsReadContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestCategoryFeeds(t *testing.T) {
expected := Feeds{
{
ID: 1,
Title: "Example",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/categories/1/feeds", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CategoryFeedsContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestDeleteCategory(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodDelete, "http://mf/v1/categories/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.DeleteCategoryContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestRefreshCategory(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/categories/1/refresh", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.RefreshCategoryContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestFeeds(t *testing.T) {
expected := Feeds{
{
ID: 1,
Title: "Example",
FeedURL: "http://example.com",
SiteURL: "http://example.com",
CheckedAt: time.Date(1970, 1, 1, 0, 7, 0, 0, time.UTC),
Disabled: false,
IgnoreHTTPCache: false,
AllowSelfSignedCertificates: false,
FetchViaProxy: false,
ScraperRules: "",
RewriteRules: "",
UrlRewriteRules: "",
BlocklistRules: "",
KeeplistRules: "",
BlockFilterEntryRules: "",
KeepFilterEntryRules: "",
Crawler: false,
UserAgent: "",
Cookie: "",
Username: "",
Password: "",
Category: &Category{
ID: 1,
Title: "Example",
},
HideGlobally: false,
DisableHTTP2: false,
ProxyURL: "",
},
{
ID: 2,
Title: "Example 2",
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FeedsContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestExport(t *testing.T) {
expected := []byte("hello")
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/export", nil, req)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(string(expected))),
Header: http.Header{},
}
})))
res, err := client.ExportContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %+v, got %+v", expected, res)
}
}
func TestImport(t *testing.T) {
expected := []byte("hello")
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(
t,
http.MethodPost,
"http://mf/v1/import",
func(r io.Reader) {
b, err := io.ReadAll(r)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !bytes.Equal(b, expected) {
t.Fatalf("expected %+v, got %+v", expected, b)
}
},
req)
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{},
}
})))
if err := client.ImportContext(t.Context(), io.NopCloser(bytes.NewBufferString(string(expected)))); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestFeed(t *testing.T) {
expected := &Feed{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FeedContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestCreateFeed(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/feeds", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, struct {
FeedID int64 `json:"feed_id"`
}{
FeedID: 1,
})
})))
id, err := client.CreateFeedContext(t.Context(), &FeedCreationRequest{
FeedURL: "http://example.com",
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if id != 1 {
t.Fatalf("Expected feed ID to be 1, got %d", id)
}
}
func TestUpdateFeed(t *testing.T) {
expected := &Feed{
ID: 1,
FeedURL: "http://example.com/",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/feeds/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UpdateFeedContext(t.Context(), 1, &FeedModificationRequest{
FeedURL: &expected.FeedURL,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestMarkFeedAsRead(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/feeds/1/mark-all-as-read", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.MarkFeedAsReadContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestRefreshAllFeeds(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/feeds/refresh", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.RefreshAllFeedsContext(t.Context()); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestRefreshFeed(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/feeds/1/refresh", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.RefreshFeedContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestDeleteFeed(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodDelete, "http://mf/v1/feeds/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.DeleteFeedContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestFeedIcon(t *testing.T) {
expected := &FeedIcon{
ID: 1,
MimeType: "text/plain",
Data: "data",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds/1/icon", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FeedIconContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestFeedEntry(t *testing.T) {
expected := &Entry{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds/1/entries/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FeedEntryContext(t.Context(), 1, 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestCategoryEntry(t *testing.T) {
expected := &Entry{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/categories/1/entries/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CategoryEntryContext(t.Context(), 1, 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestEntry(t *testing.T) {
expected := &Entry{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/entries/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.EntryContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestEntries(t *testing.T) {
expected := &EntryResultSet{
Total: 1,
Entries: Entries{
{
ID: 1,
Title: "Example",
},
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/entries", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.EntriesContext(t.Context(), nil)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestFeedEntries(t *testing.T) {
expected := &EntryResultSet{
Total: 1,
Entries: Entries{
{
ID: 1,
Title: "Example",
},
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds/1/entries?limit=10&offset=0", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FeedEntriesContext(t.Context(), 1, &Filter{
Limit: 10,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestCategoryEntries(t *testing.T) {
expected := &EntryResultSet{
Total: 1,
Entries: Entries{
{
ID: 1,
Title: "Example",
},
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/categories/1/entries?limit=10&offset=0", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.CategoryEntriesContext(t.Context(), 1, &Filter{
Limit: 10,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestUpdateEntries(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/entries", nil, req)
expectFromJSON(t, req.Body, &struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}{
EntryIDs: []int64{1, 2},
Status: "read",
})
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.UpdateEntriesContext(t.Context(), []int64{1, 2}, "read"); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestUpdateEntry(t *testing.T) {
expected := &Entry{
ID: 1,
Title: "Example",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/entries/1", nil, req)
expectFromJSON(t, req.Body, &EntryModificationRequest{
Title: &expected.Title,
})
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.UpdateEntryContext(t.Context(), 1, &EntryModificationRequest{
Title: &expected.Title,
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestToggleStarred(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/entries/1/star", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.ToggleStarredContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestSaveEntry(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPost, "http://mf/v1/entries/1/save", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.SaveEntryContext(t.Context(), 1); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestFetchEntryOriginalContent(t *testing.T) {
expected := "Example"
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/entries/1/fetch-content", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, struct {
Content string `json:"content"`
}{
Content: expected,
})
})))
res, err := client.FetchEntryOriginalContentContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if res != expected {
t.Fatalf("Expected %s, got %s", expected, res)
}
}
func TestFetchCounters(t *testing.T) {
expected := &FeedCounters{
ReadCounters: map[int64]int{
2: 1,
},
UnreadCounters: map[int64]int{
3: 1,
},
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/feeds/counters", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.FetchCountersContext(t.Context())
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestFlushHistory(t *testing.T) {
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/flush-history", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, nil)
})))
if err := client.FlushHistoryContext(t.Context()); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
func TestIcon(t *testing.T) {
expected := &FeedIcon{
ID: 1,
MimeType: "text/plain",
Data: "data",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/icons/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.IconContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestEnclosure(t *testing.T) {
expected := &Enclosure{
ID: 1,
URL: "http://example.com",
MimeType: "text/plain",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodGet, "http://mf/v1/enclosures/1", nil, req)
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
res, err := client.EnclosureContext(t.Context(), 1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Expected %s, got %s", asJSON(expected), asJSON(res))
}
}
func TestUpdateEnclosure(t *testing.T) {
expected := &Enclosure{
ID: 1,
URL: "http://example.com",
MimeType: "text/plain",
}
client := NewClientWithOptions(
"http://mf",
WithHTTPClient(
newFakeHTTPClient(t, func(t *testing.T, req *http.Request) *http.Response {
expectRequest(t, http.MethodPut, "http://mf/v1/enclosures/1", nil, req)
expectFromJSON(t, req.Body, &EnclosureUpdateRequest{
MediaProgression: 10,
})
return jsonResponseFrom(t, http.StatusOK, http.Header{}, expected)
})))
if err := client.UpdateEnclosureContext(t.Context(), 1, &EnclosureUpdateRequest{
MediaProgression: 10,
}); err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}
================================================
FILE: client/doc.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
/*
Package client implements a client library for the Miniflux REST API.
# Examples
This example fetches the list of users:
import (
miniflux "miniflux.app/v2/client"
)
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
users, err := client.Users()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(users, err)
This example discovers subscriptions on a website:
subscriptions, err := client.Discover("https://example.org/")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(subscriptions)
*/
package client // import "miniflux.app/v2/client"
================================================
FILE: client/model.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"fmt"
"time"
)
// Entry statuses.
const (
EntryStatusUnread = "unread"
EntryStatusRead = "read"
EntryStatusRemoved = "removed"
)
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"`
CustomJS string `json:"custom_js"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
GestureNav string `json:"gesture_nav"`
LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"`
CJKReadingSpeed int `json:"cjk_reading_speed"`
DefaultHomePage string `json:"default_home_page"`
CategoriesSortingOrder string `json:"categories_sorting_order"`
MarkReadOnView bool `json:"mark_read_on_view"`
MediaPlaybackRate float64 `json:"media_playback_rate"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
ExternalFontHosts string `json:"external_font_hosts"`
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
}
func (u User) String() string {
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
}
// UserCreationRequest represents the request to create a user.
type UserCreationRequest struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
}
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
IsAdmin *bool `json:"is_admin"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"`
CustomJS *string `json:"custom_js"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
GestureNav *string `json:"gesture_nav"`
DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"`
DefaultHomePage *string `json:"default_home_page"`
CategoriesSortingOrder *string `json:"categories_sorting_order"`
MarkReadOnView *bool `json:"mark_read_on_view"`
MediaPlaybackRate *float64 `json:"media_playback_rate"`
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
ExternalFontHosts *string `json:"external_font_hosts"`
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
OpenExternalLinksInNewTab *bool `json:"open_external_links_in_new_tab"`
}
// Users represents a list of users.
type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id"`
Title string `json:"title"`
UserID int64 `json:"user_id,omitempty"`
HideGlobally bool `json:"hide_globally,omitempty"`
FeedCount *int `json:"feed_count,omitempty"`
TotalUnread *int `json:"total_unread,omitempty"`
}
func (c Category) String() string {
return fmt.Sprintf("#%d %s", c.ID, c.Title)
}
// Categories represents a list of categories.
type Categories []*Category
// CategoryCreationRequest represents the request to create a category.
type CategoryCreationRequest struct {
Title string `json:"title"`
HideGlobally bool `json:"hide_globally"`
}
// CategoryModificationRequest represents the request to update a category.
type CategoryModificationRequest struct {
Title *string `json:"title"`
HideGlobally *bool `json:"hide_globally"`
}
// Subscription represents a feed subscription.
type Subscription struct {
Title string `json:"title"`
URL string `json:"url"`
Type string `json:"type"`
}
func (s Subscription) String() string {
return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
}
// Subscriptions represents a list of subscriptions.
type Subscriptions []*Subscription
// Feed represents a Miniflux feed.
type Feed struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at"`
EtagHeader string `json:"etag_header,omitempty"`
LastModifiedHeader string `json:"last_modified_header,omitempty"`
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
UrlRewriteRules string `json:"urlrewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
Crawler bool `json:"crawler"`
IgnoreEntryUpdates bool `json:"ignore_entry_updates"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Category *Category `json:"category,omitempty"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedCreationRequest represents the request to create a feed.
type FeedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
IgnoreEntryUpdates bool `json:"ignore_entry_updates"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
UrlRewriteRules string `json:"urlrewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedModificationRequest represents the request to update a feed.
type FeedModificationRequest struct {
FeedURL *string `json:"feed_url"`
SiteURL *string `json:"site_url"`
Title *string `json:"title"`
ScraperRules *string `json:"scraper_rules"`
RewriteRules *string `json:"rewrite_rules"`
UrlRewriteRules *string `json:"urlrewrite_rules"`
BlocklistRules *string `json:"blocklist_rules"`
KeeplistRules *string `json:"keeplist_rules"`
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
Crawler *bool `json:"crawler"`
IgnoreEntryUpdates *bool `json:"ignore_entry_updates"`
UserAgent *string `json:"user_agent"`
Cookie *string `json:"cookie"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
FetchViaProxy *bool `json:"fetch_via_proxy"`
HideGlobally *bool `json:"hide_globally"`
DisableHTTP2 *bool `json:"disable_http2"`
ProxyURL *string `json:"proxy_url"`
}
// FeedIcon represents the feed icon.
type FeedIcon struct {
ID int64 `json:"id"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type FeedCounters struct {
ReadCounters map[int64]int `json:"reads"`
UnreadCounters map[int64]int `json:"unreads"`
}
// Feeds represents a list of feeds.
type Feeds []*Feed
// Entry represents a subscription item in the system.
type Entry struct {
ID int64 `json:"id"`
Date time.Time `json:"published_at"`
ChangedAt time.Time `json:"changed_at"`
CreatedAt time.Time `json:"created_at"`
Feed *Feed `json:"feed,omitempty"`
Hash string `json:"hash"`
URL string `json:"url"`
CommentsURL string `json:"comments_url"`
Title string `json:"title"`
Status string `json:"status"`
Content string `json:"content"`
Author string `json:"author"`
ShareCode string `json:"share_code"`
Enclosures Enclosures `json:"enclosures,omitempty"`
Tags []string `json:"tags"`
ReadingTime int `json:"reading_time"`
UserID int64 `json:"user_id"`
FeedID int64 `json:"feed_id"`
Starred bool `json:"starred"`
}
// EntryModificationRequest represents a request to modify an entry.
type EntryModificationRequest struct {
Title *string `json:"title"`
Content *string `json:"content"`
}
// Entries represents a list of entries.
type Entries []*Entry
// Enclosure represents an attachment.
type Enclosure struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
EntryID int64 `json:"entry_id"`
URL string `json:"url"`
MimeType string `json:"mime_type"`
Size int `json:"size"`
MediaProgression int64 `json:"media_progression"`
}
type EnclosureUpdateRequest struct {
MediaProgression int64 `json:"media_progression"`
}
// Enclosures represents a list of attachments.
type Enclosures []*Enclosure
const (
FilterNotStarred = "0"
FilterOnlyStarred = "1"
)
// Filter is used to filter entries.
type Filter struct {
Status string
Offset int
Limit int
Order string
Direction string
Starred string
Before int64
After int64
PublishedBefore int64
PublishedAfter int64
ChangedBefore int64
ChangedAfter int64
BeforeEntryID int64
AfterEntryID int64
Search string
CategoryID int64
FeedID int64
Statuses []string
GloballyVisible bool
}
// EntryResultSet represents the response when fetching entries.
type EntryResultSet struct {
Total int `json:"total"`
Entries Entries `json:"entries"`
}
// VersionResponse represents the version and the build information of the Miniflux instance.
type VersionResponse struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildDate string `json:"build_date"`
GoVersion string `json:"go_version"`
Compiler string `json:"compiler"`
Arch string `json:"arch"`
OS string `json:"os"`
}
// APIKey represents an application API key.
type APIKey struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Token string `json:"token"`
Description string `json:"description"`
LastUsedAt *time.Time `json:"last_used_at"`
CreatedAt time.Time `json:"created_at"`
}
// APIKeys represents a collection of API keys.
type APIKeys []*APIKey
// APIKeyCreationRequest represents the request to create an API key.
type APIKeyCreationRequest struct {
Description string `json:"description"`
}
// SetOptionalField returns a pointer to the given value so optional request fields can be marked as set.
//
//go:fix inline
func SetOptionalField[T any](value T) *T {
return new(value)
}
================================================
FILE: client/options.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import "net/http"
type Option func(*request)
// WithAPIKey sets the API key for the client.
func WithAPIKey(apiKey string) Option {
return func(r *request) {
r.apiKey = apiKey
}
}
// WithCredentials sets the username and password for the client.
func WithCredentials(username, password string) Option {
return func(r *request) {
r.username = username
r.password = password
}
}
// WithHTTPClient sets the HTTP client for the client.
func WithHTTPClient(client *http.Client) Option {
return func(r *request) {
r.client = client
}
}
================================================
FILE: client/request.go
================================================
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
)
const (
userAgent = "Miniflux Client Library"
defaultTimeout = 80 * time.Second
)
// List of exposed errors.
var (
ErrNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
ErrForbidden = errors.New("miniflux: access forbidden")
ErrServerError = errors.New("miniflux: internal server error")
ErrNotFound = errors.New("miniflux: resource not found")
ErrBadRequest = errors.New("miniflux: bad request")
ErrEmptyEndpoint = errors.New("miniflux: empty endpoint provided")
)
type errorResponse struct {
ErrorMessage string `json:"error_message"`
}
type request struct {
endpoint string
username string
password string
apiKey string
client *http.Client
}
func (r *request) Get(ctx context.Context, path string) (io.ReadCloser, error) {
return r.execute(ctx, http.MethodGet, path, nil)
}
func (r *request) Post(ctx context.Context, path string, data any) (io.ReadCloser, error) {
return r.execute(ctx, http.MethodPost, path, data)
}
func (r *request) PostFile(ctx context.Context, path string, f io.ReadCloser) (io.ReadCloser, error) {
return r.execute(ctx, http.MethodPost, path, f)
}
func (r *request) Put(ctx context.Context, path string, data any) (io.ReadCloser, error) {
return r.execute(ctx, http.MethodPut, path, data)
}
func (r *request) Delete(ctx context.Context, path string) error {
_, err := r.execute(ctx, http.MethodDelete, path, nil)
return err
}
func (r *request) execute(
ctx context.Context,
method string,
path string,
data any,
) (io.ReadCloser, error) {
if r.endpoint == "" {
return nil, ErrEmptyEndpoint
}
if r.endpoint[len(r.endpoint)-1:] == "/" {
r.endpoint = r.endpoint[:len(r.endpoint)-1]
}
u, err := url.Parse(r.endpoint + path)
if err != nil {
return nil, err
}
request, err := http.NewRequestWithContext(ctx, method, u.String(), nil)
if err != nil {
return nil, err
}
request.Header = r.buildHeaders()
if r.username != "" && r.password != "" {
request.SetBasicAuth(r.username, r.password)
}
if data != nil {
switch data := data.(type) {
case io.ReadCloser:
request.Body = data
default:
request.Body = io.NopCloser(bytes.NewBuffer(r.toJSON(data)))
}
}
client := r.client
response, err := client.Do(request)
if err != nil {
return nil, err
}
switch response.StatusCode {
case http.StatusUnauthorized:
response.Body.Close()
return nil, ErrNotAuthorized
case http.StatusForbidden:
response.Body.Close()
return nil, ErrForbidden
case http.StatusInternalServerError:
defer response.Body.Close()
var resp errorResponse
decoder := json.NewDecoder(response.Body)
// If we failed to decode, just return a generic ErrServerError
if err := decoder.Decode(&resp); err != nil {
return nil, ErrServerError
}
return nil, errors.New("miniflux: internal server error: " + resp.ErrorMessage)
case http.StatusNotFound:
response.Body.Close()
return nil, ErrNotFound
case http.StatusNoContent:
response.Body.Close()
return nil, nil
case http.StatusBadRequest:
defer response.Body.Close()
var resp errorResponse
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&resp); err != nil {
return nil, fmt.Errorf("%w (%v)", ErrBadRequest, err)
}
return nil, fmt.Errorf("%w (%s)", ErrBadRequest, resp.ErrorMessage)
}
if response.StatusCode > 400 {
response.Body.Close()
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
}
return response.Body, nil
}
func (r *request) buildHeaders() http.Header {
headers := make(http.Header)
headers.Add("User-Agent", userAgent)
headers.Add("Content-Type", "application/json")
headers.Add("Accept", "application/json")
if r.apiKey != "" {
headers.Add("X-Auth-Token", r.apiKey)
}
return headers
}
func (r *request) toJSON(v any) []byte {
b, err := json.Marshal(v)
if err != nil {
log.Println("Unable to convert interface to JSON:", err)
return []byte("")
}
return b
}
================================================
FILE: contrib/README.md
================================================
The contrib directory contains various useful things contributed by the community.
Community contributions are not officially supported by the maintainers.
There is no guarantee whatsoever that anything in this folder works.
================================================
FILE: contrib/ansible/inventories/group_vars/miniflux_vars.yml
================================================
---
miniflux_linux_user: miniflux
miniflux_db_user_name: miniflux_db_user
miniflux_db_user_password: miniflux_db_user_password
miniflux_db: miniflux_db
miniflux_admin_name: admin
miniflux_admin_passwort: miniflux_admin_password
miniflux_port: 8080
================================================
FILE: contrib/ansible/playbooks/playbook.yml
================================================
---
- hosts: miniflux
roles:
- { role: mgrote.miniflux, tags: "miniflux" }
================================================
FILE: contrib/ansible/roles/mgrote.miniflux/README.md
================================================
## mgrote.miniflux
### Details
Installs and configures Miniflux v2 with ansible
### Works on...
- [x] Ubuntu (>=18.04)
### Variables and Defaults
##### Linux User
miniflux_linux_user: miniflux
##### DB User
miniflux_db_user_name: miniflux_db_user
##### DB Password
miniflux_db_user_password: qqqqqqqqqqqqq
##### Database
miniflux_db: miniflux_db
##### Username Miniflux Admin
miniflux_admin_name: admin
##### Password Miniflux Admin
miniflux_admin_passwort: hallowelt
##### Port for Miniflux Frontend
miniflux_port: 8080
================================================
FILE: contrib/ansible/roles/mgrote.miniflux/defaults/main.yml
================================================
================================================
FILE: contrib/ansible/roles/mgrote.miniflux/handlers/main.yml
================================================
---
- name: start_miniflux.service
become: yes
systemd:
name: miniflux
state: restarted
enabled: yes
# wait 15 seconds(for systemd)
- name: miniflux_wait
wait_for:
timeout: 15
================================================
FILE: contrib/ansible/roles/mgrote.miniflux/tasks/main.yml
================================================
- name: add Apt-key for miniflux-repo
become: yes
apt_key:
url: https://apt.miniflux.app/KEY.gpg
state: present
- name: add miniflux-repo
become: yes
apt_repository:
repo: 'deb https://apt.miniflux.app/ /'
state: present
filename: miniflux_repo
update_cache: yes
- name: install miniflux
become: yes
apt:
name: miniflux
state: present
- name: add miniflux linux_user
become: yes
user:
name: "{{ miniflux_linux_user }}"
home: "/var/empty"
create_home: "no"
system: "yes"
shell: "/bin/false"
- name: create directory "/etc/miniflux.d"
become: yes
file:
path: /etc/miniflux.d
state: directory
- name: copy miniflux.conf
become: yes
template:
src: "miniflux.conf"
dest: "/etc/miniflux.conf"
notify:
- start_miniflux.service
- miniflux_wait
================================================
FILE: contrib/ansible/roles/mgrote.miniflux/templates/miniflux.conf
================================================
# See https://docs.miniflux.app/
LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
POLLING_FREQUENCY=15
PROXY_IMAGES=http-only
# Run SQL migrations automatically:
RUN_MIGRATIONS=1
CREATE_ADMIN=1
ADMIN_USERNAME={{ miniflux_admin_name }}
ADMIN_PASSWORD={{ miniflux_admin_passwort }}
POLLING_FREQUENCY=10
# Options: https://miniflux.app/miniflux.1.html
================================================
FILE: contrib/bruno/README.md
================================================
This folder contains Miniflux API collection for [Bruno](https://www.usebruno.com).
Bruno is a lightweight alternative to Postman/Insomnia.
- https://www.usebruno.com
- https://github.com/usebruno/bruno
================================================
FILE: contrib/bruno/miniflux/Bookmark an entry.bru
================================================
meta {
name: Bookmark an entry
type: http
seq: 37
}
put {
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/bookmark
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
entryID: 1698
}
================================================
FILE: contrib/bruno/miniflux/Create a feed.bru
================================================
meta {
name: Create a feed
type: http
seq: 19
}
post {
url: {{minifluxBaseURL}}/v1/feeds
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
================================================
FILE: contrib/bruno/miniflux/Create a new category.bru
================================================
meta {
name: Create a new category
type: http
seq: 10
}
post {
url: {{minifluxBaseURL}}/v1/categories
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test"
}
}
================================================
FILE: contrib/bruno/miniflux/Create a new user.bru
================================================
meta {
name: Create a new user
type: http
seq: 5
}
post {
url: {{minifluxBaseURL}}/v1/users
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"username": "foobar",
"password": "secret123"
}
}
================================================
FILE: contrib/bruno/miniflux/Delete a category.bru
================================================
meta {
name: Delete a category
type: http
seq: 12
}
delete {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 1
}
================================================
FILE: contrib/bruno/miniflux/Delete a feed.bru
================================================
meta {
name: Delete a feed
type: http
seq: 26
}
delete {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
feedID: 18
}
================================================
FILE: contrib/bruno/miniflux/Delete a user.bru
================================================
meta {
name: Delete a user
type: http
seq: 7
}
delete {
url: {{minifluxBaseURL}}/v1/users/{{userID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"language": "fr_FR"
}
}
vars:pre-request {
userID: 2
}
================================================
FILE: contrib/bruno/miniflux/Discover feeds.bru
================================================
meta {
name: Discover feeds
type: http
seq: 18
}
post {
url: {{minifluxBaseURL}}/v1/discover
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"url": "https://miniflux.app"
}
}
================================================
FILE: contrib/bruno/miniflux/Fetch entry website content.bru
================================================
meta {
name: Fetch entry website content
type: http
seq: 39
}
get {
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/fetch-content
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
entryID: 1698
}
================================================
FILE: contrib/bruno/miniflux/Flush history.bru
================================================
meta {
name: Flush history
type: http
seq: 40
}
put {
url: {{minifluxBaseURL}}/v1/flush-history
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"url": "https://miniflux.app"
}
}
================================================
FILE: contrib/bruno/miniflux/Get a single entry.bru
================================================
meta {
name: Get a single entry
type: http
seq: 36
}
get {
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
entryID: 1698
}
================================================
FILE: contrib/bruno/miniflux/Get a single feed entry.bru
================================================
meta {
name: Get a single feed entry
type: http
seq: 33
}
get {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries/{{entryID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
feedID: 19
entryID: 1698
}
================================================
FILE: contrib/bruno/miniflux/Get a single feed.bru
================================================
meta {
name: Get a single feed
type: http
seq: 24
}
get {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
feedID: 18
}
================================================
FILE: contrib/bruno/miniflux/Get a single user by ID.bru
================================================
meta {
name: Get a single user by ID
type: http
seq: 3
}
get {
url: {{minifluxBaseURL}}/v1/users/{{userID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
vars:pre-request {
userID: 1
}
================================================
FILE: contrib/bruno/miniflux/Get a single user by username.bru
================================================
meta {
name: Get a single user by username
type: http
seq: 4
}
get {
url: {{minifluxBaseURL}}/v1/users/{{username}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
vars:pre-request {
username: admin
}
================================================
FILE: contrib/bruno/miniflux/Get all categories.bru
================================================
meta {
name: Get all categories
type: http
seq: 9
}
get {
url: {{minifluxBaseURL}}/v1/categories
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
================================================
FILE: contrib/bruno/miniflux/Get all entries.bru
================================================
meta {
name: Get all entries
type: http
seq: 34
}
get {
url: {{minifluxBaseURL}}/v1/entries
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
================================================
FILE: contrib/bruno/miniflux/Get all feeds.bru
================================================
meta {
name: Get all feeds
type: http
seq: 20
}
get {
url: {{minifluxBaseURL}}/v1/feeds
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
================================================
FILE: contrib/bruno/miniflux/Get all users.bru
================================================
meta {
name: Get all users
type: http
seq: 2
}
get {
url: {{minifluxBaseURL}}/v1/users
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
================================================
FILE: contrib/bruno/miniflux/Get category entries.bru
================================================
meta {
name: Get category entries
type: http
seq: 16
}
get {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 2
}
================================================
FILE: contrib/bruno/miniflux/Get category entry.bru
================================================
meta {
name: Get category entry
type: http
seq: 17
}
get {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries/{{entryID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 2
entryID: 1
}
================================================
FILE: contrib/bruno/miniflux/Get category feeds.bru
================================================
meta {
name: Get category feeds
type: http
seq: 14
}
get {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/feeds
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 2
}
================================================
FILE: contrib/bruno/miniflux/Get current user.bru
================================================
meta {
name: Get current user
type: http
seq: 1
}
get {
url: {{minifluxBaseURL}}/v1/me
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
================================================
FILE: contrib/bruno/miniflux/Get feed counters.bru
================================================
meta {
name: Get feed counters
type: http
seq: 21
}
get {
url: {{minifluxBaseURL}}/v1/feeds/counters
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
================================================
FILE: contrib/bruno/miniflux/Get feed entries.bru
================================================
meta {
name: Get feed entries
type: http
seq: 32
}
get {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
feedID: 19
}
================================================
FILE: contrib/bruno/miniflux/Get feed icon by feed ID.bru
================================================
meta {
name: Get feed icon by feed ID
type: http
seq: 27
}
get {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/icon
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
feedID: 19
}
================================================
FILE: contrib/bruno/miniflux/Get feed icon by icon ID.bru
================================================
meta {
name: Get feed icon by icon ID
type: http
seq: 28
}
get {
url: {{minifluxBaseURL}}/v1/icons/{{iconID}}
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
iconID: 11
}
================================================
FILE: contrib/bruno/miniflux/Get version and build information.bru
================================================
meta {
name: Get version and build information
type: http
seq: 42
}
get {
url: {{minifluxBaseURL}}/v1/version
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
================================================
FILE: contrib/bruno/miniflux/Mark all category entries as read.bru
================================================
meta {
name: Mark all category entries as read
type: http
seq: 13
}
put {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/mark-all-as-read
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 2
}
================================================
FILE: contrib/bruno/miniflux/Mark all user entries as read.bru
================================================
meta {
name: Mark all user entries as read
type: http
seq: 8
}
put {
url: {{minifluxBaseURL}}/v1/users/{{userID}}/mark-all-as-read
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
userID: 1
}
================================================
FILE: contrib/bruno/miniflux/Mark feed as read.bru
================================================
meta {
name: Mark feed as read
type: http
seq: 29
}
put {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/mark-all-as-read
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
feedID: 19
}
================================================
FILE: contrib/bruno/miniflux/OPML Export.bru
================================================
meta {
name: OPML Export
type: http
seq: 30
}
get {
url: {{minifluxBaseURL}}/v1/export
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
feedID: 19
}
================================================
FILE: contrib/bruno/miniflux/OPML Import.bru
================================================
meta {
name: OPML Import
type: http
seq: 31
}
post {
url: {{minifluxBaseURL}}/v1/import
body: xml
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
body:xml {
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Miniflux</title>
</head>
<body>
<outline text="My category">
<outline title="Miniflux" text="Miniflux" xmlUrl="https://miniflux.app/feed.xml" htmlUrl="https://miniflux.app"></outline>
</outline>
</body>
</opml>
}
vars:pre-request {
feedID: 19
}
================================================
FILE: contrib/bruno/miniflux/Refresh a single feed.bru
================================================
meta {
name: Refresh a single feed
type: http
seq: 23
}
put {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/refresh
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
feedID: 18
}
================================================
FILE: contrib/bruno/miniflux/Refresh all feeds.bru
================================================
meta {
name: Refresh all feeds
type: http
seq: 22
}
put {
url: {{minifluxBaseURL}}/v1/feeds/refresh
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
================================================
FILE: contrib/bruno/miniflux/Refresh category feeds.bru
================================================
meta {
name: Refresh category feeds
type: http
seq: 15
}
put {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/refresh
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 2
}
================================================
FILE: contrib/bruno/miniflux/Save an entry.bru
================================================
meta {
name: Save an entry
type: http
seq: 38
}
post {
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/save
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"feed_url": "https://miniflux.app/feed.xml"
}
}
vars:pre-request {
entryID: 1698
}
================================================
FILE: contrib/bruno/miniflux/Update a category.bru
================================================
meta {
name: Update a category
type: http
seq: 11
}
put {
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "Test Update"
}
}
vars:pre-request {
categoryID: 1
}
================================================
FILE: contrib/bruno/miniflux/Update a feed.bru
================================================
meta {
name: Update a feed
type: http
seq: 25
}
put {
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"user_agent": "My user agent"
}
}
vars:pre-request {
feedID: 18
}
================================================
FILE: contrib/bruno/miniflux/Update a user.bru
================================================
meta {
name: Update a user
type: http
seq: 6
}
put {
url: {{minifluxBaseURL}}/v1/users/{{userID}}
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"language": "fr_FR"
}
}
vars:pre-request {
userID: 1
}
================================================
FILE: contrib/bruno/miniflux/Update entries status.bru
================================================
meta {
name: Update entries status
type: http
seq: 35
}
put {
url: {{minifluxBaseURL}}/v1/entries
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"entry_ids": [1698, 1699],
"status": "read"
}
}
================================================
FILE: contrib/bruno/miniflux/Update entry.bru
================================================
meta {
name: Update entry
type: http
seq: 41
}
put {
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
body: json
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}
body:json {
{
"title": "New title",
"content": "Some text"
}
}
vars:pre-request {
entryID: 1789
}
================================================
FILE: contrib/bruno/miniflux/bruno.json
================================================
{
"version": "1",
"name": "Miniflux",
"type": "collection"
}
================================================
FILE: contrib/bruno/miniflux/environments/Local.bru
================================================
vars {
minifluxBaseURL: http://127.0.0.1:8080
minifluxUsername: admin
}
vars:secret [
minifluxPassword
]
================================================
FILE: contrib/docker-compose/Caddyfile
================================================
miniflux.example.org
reverse_proxy miniflux:8080
================================================
FILE: contrib/docker-compose/README.md
================================================
Docker-Compose Examples
=======================
Here are few Docker Compose examples:
- `basic.yml`: Basic example
- `caddy.yml`: Use Caddy as reverse-proxy with automatic HTTPS
- `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
```bash
docker compose -f basic.yml up -d
```
================================================
FILE: contrib/docker-compose/basic.yml
================================================
services:
miniflux:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
restart: always
ports:
- "80:8080"
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- DEBUG=1
# Optional health check:
# healthcheck:
# test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=miniflux
volumes:
- miniflux-db:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux"]
interval: 10s
start_period: 30s
volumes:
miniflux-db:
================================================
FILE: contrib/docker-compose/caddy.yml
================================================
services:
caddy:
image: caddy:2
container_name: caddy
depends_on:
- miniflux
ports:
- "80:80"
- "443:443"
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
miniflux:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- BASE_URL=https://miniflux.example.org
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux"]
interval: 10s
start_period: 30s
volumes:
miniflux-db:
caddy_data:
caddy_config:
================================================
FILE: contrib/docker-compose/traefik.yml
================================================
services:
traefik:
image: "traefik:v2.3"
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
depends_on:
- miniflux
ports:
- "443:443"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
miniflux:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
depends_on:
db:
condition: service_healthy
expose:
- "8080"
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- BASE_URL=https://miniflux.example.org
labels:
- "traefik.enable=true"
- "traefik.http.routers.miniflux.rule=Host(`miniflux.example.org`)"
- "traefik.http.routers.miniflux.entrypoints=websecure"
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux"]
interval: 10s
start_period: 30s
volumes:
miniflux-db:
================================================
FILE: contrib/grafana/README.md
================================================
Grafana Dashboard for Miniflux
================================================
FILE: contrib/grafana/dashboard.json
================================================
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__elements": {},
"__requires": [
{
"type": "panel",
"id": "bargauge",
"name": "Bar gauge",
"version": ""
},
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "10.4.3"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "stat",
"name": "Stat",
"version": ""
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"collapsed": false,
"datasource": {
"uid": "Prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 24,
"panels": [],
"targets": [
{
"datasource": {
"uid": "Prometheus"
},
"refId": "A"
}
],
"title": "Application",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 8,
"x": 0,
"y": 1
},
"id": 18,
"options": {
"displayMode": "basic",
"maxVizHeight": 300,
"minVizHeight": 16,
"minVizWidth": 8,
"namePlacement": "auto",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"showUnfilled": true,
"sizing": "auto",
"valueMode": "color"
},
"pluginVersion": "10.4.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_feeds{status=\"total\"})",
"hide": false,
"interval": "",
"legendFormat": "Total",
"refId": "D"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_feeds{status=\"enabled\"})",
"hide": false,
"interval": "",
"legendFormat": "Enabled",
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_broken_feeds)",
"interval": "",
"legendFormat": "Broken",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_feeds{status=\"disabled\"})",
"interval": "",
"legendFormat": "Disabled",
"refId": "B"
}
],
"title": "Feeds",
"type": "bargauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 4,
"x": 8,
"y": 1
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.4.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_users)",
"interval": "",
"legendFormat": "Users",
"refId": "A"
}
],
"title": "Users",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 50,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "10.4.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_entries{status=\"total\"})",
"hide": false,
"interval": "",
"legendFormat": "Total",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_entries{status=\"unread\"})",
"hide": false,
"interval": "",
"legendFormat": "Unread",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_entries{status=\"read\"})",
"interval": "",
"legendFormat": "Read",
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "max(miniflux_entries{status=\"removed\"})",
"interval": "",
"legendFormat": "Removed",
"refId": "D"
}
],
"title": "Entries by Status",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "decbytes"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 4,
"x": 8,
"y": 4
},
"id": 36,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "vertical",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "10.4.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "go_memstats_sys_bytes{job=\"miniflux\"}",
"interval": "",
"legendFormat": "{{ instance }} - Memory Used",
"refId": "A"
}
],
"type": "stat"
},
{
"datasource": {
"type": "prometheu
gitextract_58fdb7bz/
├── .devcontainer/
│ ├── devcontainer.json
│ └── docker-compose.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── documentation.yml
│ │ ├── feature_request.yml
│ │ ├── feed_issue.yml
│ │ └── proposal.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build_binaries.yml
│ ├── codeberg_mirror.yml
│ ├── codeql-analysis.yml
│ ├── debian_packages.yml
│ ├── docker.yml
│ ├── linters.yml
│ ├── rpm_packages.yml
│ ├── scripts/
│ │ └── commit-checker.py
│ └── tests.yml
├── .gitignore
├── .golangci.yml
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── Procfile
├── README.md
├── SECURITY.md
├── client/
│ ├── README.md
│ ├── client.go
│ ├── client_test.go
│ ├── doc.go
│ ├── model.go
│ ├── options.go
│ └── request.go
├── contrib/
│ ├── README.md
│ ├── ansible/
│ │ ├── inventories/
│ │ │ └── group_vars/
│ │ │ └── miniflux_vars.yml
│ │ ├── playbooks/
│ │ │ └── playbook.yml
│ │ └── roles/
│ │ └── mgrote.miniflux/
│ │ ├── README.md
│ │ ├── defaults/
│ │ │ └── main.yml
│ │ ├── handlers/
│ │ │ └── main.yml
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ └── templates/
│ │ └── miniflux.conf
│ ├── bruno/
│ │ ├── README.md
│ │ └── miniflux/
│ │ ├── Bookmark an entry.bru
│ │ ├── Create a feed.bru
│ │ ├── Create a new category.bru
│ │ ├── Create a new user.bru
│ │ ├── Delete a category.bru
│ │ ├── Delete a feed.bru
│ │ ├── Delete a user.bru
│ │ ├── Discover feeds.bru
│ │ ├── Fetch entry website content.bru
│ │ ├── Flush history.bru
│ │ ├── Get a single entry.bru
│ │ ├── Get a single feed entry.bru
│ │ ├── Get a single feed.bru
│ │ ├── Get a single user by ID.bru
│ │ ├── Get a single user by username.bru
│ │ ├── Get all categories.bru
│ │ ├── Get all entries.bru
│ │ ├── Get all feeds.bru
│ │ ├── Get all users.bru
│ │ ├── Get category entries.bru
│ │ ├── Get category entry.bru
│ │ ├── Get category feeds.bru
│ │ ├── Get current user.bru
│ │ ├── Get feed counters.bru
│ │ ├── Get feed entries.bru
│ │ ├── Get feed icon by feed ID.bru
│ │ ├── Get feed icon by icon ID.bru
│ │ ├── Get version and build information.bru
│ │ ├── Mark all category entries as read.bru
│ │ ├── Mark all user entries as read.bru
│ │ ├── Mark feed as read.bru
│ │ ├── OPML Export.bru
│ │ ├── OPML Import.bru
│ │ ├── Refresh a single feed.bru
│ │ ├── Refresh all feeds.bru
│ │ ├── Refresh category feeds.bru
│ │ ├── Save an entry.bru
│ │ ├── Update a category.bru
│ │ ├── Update a feed.bru
│ │ ├── Update a user.bru
│ │ ├── Update entries status.bru
│ │ ├── Update entry.bru
│ │ ├── bruno.json
│ │ └── environments/
│ │ └── Local.bru
│ ├── docker-compose/
│ │ ├── Caddyfile
│ │ ├── README.md
│ │ ├── basic.yml
│ │ ├── caddy.yml
│ │ └── traefik.yml
│ ├── grafana/
│ │ ├── README.md
│ │ └── dashboard.json
│ ├── sysvinit/
│ │ ├── README.md
│ │ └── etc/
│ │ ├── default/
│ │ │ └── miniflux
│ │ └── init.d/
│ │ └── miniflux
│ └── thunder_client/
│ ├── README.md
│ └── collection.json
├── go.mod
├── go.sum
├── internal/
│ ├── api/
│ │ ├── api.go
│ │ ├── api_integration_test.go
│ │ ├── api_key_handlers.go
│ │ ├── api_test.go
│ │ ├── category_handlers.go
│ │ ├── enclosure_handlers.go
│ │ ├── entry_handlers.go
│ │ ├── feed_handlers.go
│ │ ├── icon_handlers.go
│ │ ├── messages.go
│ │ ├── middleware.go
│ │ ├── opml_handlers.go
│ │ ├── subscription_handlers.go
│ │ ├── user_handlers.go
│ │ └── version_handler.go
│ ├── cli/
│ │ ├── ask_credentials.go
│ │ ├── cleanup_tasks.go
│ │ ├── cli.go
│ │ ├── create_admin.go
│ │ ├── daemon.go
│ │ ├── export_feeds.go
│ │ ├── flush_sessions.go
│ │ ├── health_check.go
│ │ ├── info.go
│ │ ├── logger.go
│ │ ├── refresh_feeds.go
│ │ ├── reset_password.go
│ │ └── scheduler.go
│ ├── config/
│ │ ├── config.go
│ │ ├── options.go
│ │ ├── options_parsing_test.go
│ │ ├── parser.go
│ │ ├── parser_test.go
│ │ ├── validators.go
│ │ └── validators_test.go
│ ├── crypto/
│ │ └── crypto.go
│ ├── database/
│ │ ├── database.go
│ │ ├── migrations.go
│ │ └── postgresql.go
│ ├── fever/
│ │ ├── README.md
│ │ ├── handler.go
│ │ ├── middleware.go
│ │ └── response.go
│ ├── googlereader/
│ │ ├── README.md
│ │ ├── handler.go
│ │ ├── item.go
│ │ ├── item_test.go
│ │ ├── middleware.go
│ │ ├── parameters.go
│ │ ├── prefix_suffix.go
│ │ ├── request_modifier.go
│ │ ├── response.go
│ │ └── stream.go
│ ├── http/
│ │ ├── client/
│ │ │ ├── client.go
│ │ │ └── client_test.go
│ │ ├── cookie/
│ │ │ └── cookie.go
│ │ ├── request/
│ │ │ ├── client_ip.go
│ │ │ ├── client_ip_test.go
│ │ │ ├── context.go
│ │ │ ├── context_test.go
│ │ │ ├── cookie.go
│ │ │ ├── cookie_test.go
│ │ │ ├── params.go
│ │ │ └── params_test.go
│ │ ├── response/
│ │ │ ├── builder.go
│ │ │ ├── builder_test.go
│ │ │ ├── html.go
│ │ │ ├── html_test.go
│ │ │ ├── json.go
│ │ │ ├── json_test.go
│ │ │ ├── response.go
│ │ │ ├── response_test.go
│ │ │ ├── text.go
│ │ │ ├── text_test.go
│ │ │ ├── xml.go
│ │ │ └── xml_test.go
│ │ └── server/
│ │ ├── healthcheck.go
│ │ ├── httpd.go
│ │ ├── metrics.go
│ │ ├── middleware.go
│ │ └── routes.go
│ ├── integration/
│ │ ├── apprise/
│ │ │ └── apprise.go
│ │ ├── archiveorg/
│ │ │ └── archiveorg.go
│ │ ├── betula/
│ │ │ └── betula.go
│ │ ├── cubox/
│ │ │ └── cubox.go
│ │ ├── discord/
│ │ │ └── discord.go
│ │ ├── espial/
│ │ │ └── espial.go
│ │ ├── instapaper/
│ │ │ └── instapaper.go
│ │ ├── integration.go
│ │ ├── integration_test.go
│ │ ├── karakeep/
│ │ │ └── karakeep.go
│ │ ├── linkace/
│ │ │ └── linkace.go
│ │ ├── linkding/
│ │ │ └── linkding.go
│ │ ├── linktaco/
│ │ │ ├── linktaco.go
│ │ │ └── linktaco_test.go
│ │ ├── linkwarden/
│ │ │ ├── linkwarden.go
│ │ │ └── linkwarden_test.go
│ │ ├── matrixbot/
│ │ │ ├── client.go
│ │ │ └── matrixbot.go
│ │ ├── notion/
│ │ │ └── notion.go
│ │ ├── ntfy/
│ │ │ └── ntfy.go
│ │ ├── nunuxkeeper/
│ │ │ └── nunuxkeeper.go
│ │ ├── omnivore/
│ │ │ └── omnivore.go
│ │ ├── pinboard/
│ │ │ ├── pinboard.go
│ │ │ └── post.go
│ │ ├── pushover/
│ │ │ └── pushover.go
│ │ ├── raindrop/
│ │ │ └── raindrop.go
│ │ ├── readeck/
│ │ │ ├── readeck.go
│ │ │ └── readeck_test.go
│ │ ├── readwise/
│ │ │ └── readwise.go
│ │ ├── rssbridge/
│ │ │ └── rssbridge.go
│ │ ├── shaarli/
│ │ │ └── shaarli.go
│ │ ├── shiori/
│ │ │ └── shiori.go
│ │ ├── slack/
│ │ │ └── slack.go
│ │ ├── telegrambot/
│ │ │ ├── client.go
│ │ │ └── telegrambot.go
│ │ ├── wallabag/
│ │ │ ├── wallabag.go
│ │ │ └── wallabag_test.go
│ │ └── webhook/
│ │ └── webhook.go
│ ├── locale/
│ │ ├── catalog.go
│ │ ├── catalog_test.go
│ │ ├── error.go
│ │ ├── error_test.go
│ │ ├── locale.go
│ │ ├── locale_test.go
│ │ ├── plural.go
│ │ ├── plural_test.go
│ │ ├── printer.go
│ │ ├── printer_test.go
│ │ └── translations/
│ │ ├── ar_SA.json
│ │ ├── de_DE.json
│ │ ├── el_EL.json
│ │ ├── en_US.json
│ │ ├── es_ES.json
│ │ ├── fi_FI.json
│ │ ├── fr_FR.json
│ │ ├── gl_ES.json
│ │ ├── hi_IN.json
│ │ ├── id_ID.json
│ │ ├── it_IT.json
│ │ ├── ja_JP.json
│ │ ├── nan_Latn_pehoeji.json
│ │ ├── nl_NL.json
│ │ ├── pl_PL.json
│ │ ├── pt_BR.json
│ │ ├── ro_RO.json
│ │ ├── ru_RU.json
│ │ ├── tr_TR.json
│ │ ├── uk_UA.json
│ │ ├── zh_CN.json
│ │ └── zh_TW.json
│ ├── mediaproxy/
│ │ ├── media_proxy_test.go
│ │ ├── rewriter.go
│ │ └── url.go
│ ├── metric/
│ │ └── metric.go
│ ├── model/
│ │ ├── api_key.go
│ │ ├── app_session.go
│ │ ├── categories_sort_options.go
│ │ ├── category.go
│ │ ├── enclosure.go
│ │ ├── enclosure_test.go
│ │ ├── entry.go
│ │ ├── feed.go
│ │ ├── feed_test.go
│ │ ├── home_page.go
│ │ ├── icon.go
│ │ ├── integration.go
│ │ ├── job.go
│ │ ├── model.go
│ │ ├── subscription.go
│ │ ├── theme.go
│ │ ├── user.go
│ │ ├── user_session.go
│ │ └── webauthn.go
│ ├── oauth2/
│ │ ├── authorization.go
│ │ ├── google.go
│ │ ├── manager.go
│ │ ├── oidc.go
│ │ ├── profile.go
│ │ └── provider.go
│ ├── proxyrotator/
│ │ ├── proxyrotator.go
│ │ └── proxyrotator_test.go
│ ├── reader/
│ │ ├── atom/
│ │ │ ├── atom_03.go
│ │ │ ├── atom_03_adapter.go
│ │ │ ├── atom_03_test.go
│ │ │ ├── atom_10.go
│ │ │ ├── atom_10_adapter.go
│ │ │ ├── atom_10_test.go
│ │ │ ├── atom_common.go
│ │ │ └── parser.go
│ │ ├── date/
│ │ │ ├── parser.go
│ │ │ └── parser_test.go
│ │ ├── dublincore/
│ │ │ └── dublincore.go
│ │ ├── encoding/
│ │ │ ├── encoding.go
│ │ │ ├── encoding_test.go
│ │ │ └── testdata/
│ │ │ ├── invalid-prolog.xml
│ │ │ ├── iso-8859-1-meta-after-1024.html
│ │ │ ├── iso-8859-1.html
│ │ │ ├── iso-8859-1.xml
│ │ │ ├── koi8r.xml
│ │ │ ├── utf8-incorrect-prolog.xml
│ │ │ ├── utf8-meta-after-1024.html
│ │ │ ├── utf8.html
│ │ │ ├── utf8.xml
│ │ │ ├── windows-1252-incorrect-prolog.xml
│ │ │ └── windows-1252.xml
│ │ ├── fetcher/
│ │ │ ├── encoding_wrappers.go
│ │ │ ├── request_builder.go
│ │ │ ├── request_builder_test.go
│ │ │ ├── response_handler.go
│ │ │ └── response_handler_test.go
│ │ ├── filter/
│ │ │ ├── filter.go
│ │ │ └── filter_test.go
│ │ ├── googleplay/
│ │ │ └── googleplay.go
│ │ ├── handler/
│ │ │ └── handler.go
│ │ ├── icon/
│ │ │ ├── checker.go
│ │ │ ├── finder.go
│ │ │ └── finder_test.go
│ │ ├── itunes/
│ │ │ └── itunes.go
│ │ ├── json/
│ │ │ ├── adapter.go
│ │ │ ├── json.go
│ │ │ ├── parser.go
│ │ │ └── parser_test.go
│ │ ├── media/
│ │ │ ├── media.go
│ │ │ └── media_test.go
│ │ ├── opml/
│ │ │ ├── handler.go
│ │ │ ├── opml.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ ├── serializer.go
│ │ │ ├── serializer_test.go
│ │ │ └── subscription.go
│ │ ├── parser/
│ │ │ ├── format.go
│ │ │ ├── format_test.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ └── testdata/
│ │ │ ├── encoding_ISO-8859-1.xml
│ │ │ ├── encoding_WINDOWS-1251.xml
│ │ │ ├── large_atom.xml
│ │ │ ├── large_rss.xml
│ │ │ ├── no_encoding_ISO-8859-1.xml
│ │ │ ├── rdf_UTF8.xml
│ │ │ ├── small_atom.xml
│ │ │ └── urdu_UTF8.xml
│ │ ├── processor/
│ │ │ ├── bilibili.go
│ │ │ ├── nebula.go
│ │ │ ├── odysee.go
│ │ │ ├── processor.go
│ │ │ ├── reading_time.go
│ │ │ ├── utils.go
│ │ │ ├── utils_test.go
│ │ │ ├── youtube.go
│ │ │ └── youtube_test.go
│ │ ├── rdf/
│ │ │ ├── adapter.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ └── rdf.go
│ │ ├── readability/
│ │ │ ├── readability.go
│ │ │ └── readability_test.go
│ │ ├── readingtime/
│ │ │ ├── readingtime.go
│ │ │ └── readingtime_test.go
│ │ ├── rewrite/
│ │ │ ├── content_rewrite.go
│ │ │ ├── content_rewrite_functions.go
│ │ │ ├── content_rewrite_rules.go
│ │ │ ├── content_rewrite_test.go
│ │ │ ├── referer_override.go
│ │ │ ├── referer_override_test.go
│ │ │ ├── url_rewrite.go
│ │ │ └── url_rewrite_test.go
│ │ ├── rss/
│ │ │ ├── adapter.go
│ │ │ ├── atom.go
│ │ │ ├── feedburner.go
│ │ │ ├── parser.go
│ │ │ ├── parser_test.go
│ │ │ ├── podcast.go
│ │ │ └── rss.go
│ │ ├── sanitizer/
│ │ │ ├── sanitizer.go
│ │ │ ├── sanitizer_test.go
│ │ │ ├── srcset.go
│ │ │ ├── srcset_test.go
│ │ │ ├── strip_tags.go
│ │ │ ├── strip_tags_test.go
│ │ │ ├── testdata/
│ │ │ │ ├── miniflux_github.html
│ │ │ │ └── miniflux_wikipedia.html
│ │ │ ├── truncate.go
│ │ │ └── truncate_test.go
│ │ ├── scraper/
│ │ │ ├── rules.go
│ │ │ ├── scraper.go
│ │ │ ├── scraper_test.go
│ │ │ └── testdata/
│ │ │ ├── iframe.html
│ │ │ ├── iframe.html-result
│ │ │ ├── img.html
│ │ │ ├── img.html-result
│ │ │ ├── p.html
│ │ │ └── p.html-result
│ │ ├── subscription/
│ │ │ ├── finder.go
│ │ │ ├── finder_test.go
│ │ │ └── subscription.go
│ │ ├── urlcleaner/
│ │ │ ├── urlcleaner.go
│ │ │ └── urlcleaner_test.go
│ │ └── xml/
│ │ ├── decoder.go
│ │ ├── decoder_test.go
│ │ └── testdata/
│ │ ├── iso88591.xml
│ │ ├── iso88591_utf8_mismatch.xml
│ │ └── koi8r.xml
│ ├── storage/
│ │ ├── api_key.go
│ │ ├── batch.go
│ │ ├── category.go
│ │ ├── certificate_cache.go
│ │ ├── enclosure.go
│ │ ├── entry.go
│ │ ├── entry_pagination_builder.go
│ │ ├── entry_query_builder.go
│ │ ├── entry_test.go
│ │ ├── feed.go
│ │ ├── feed_query_builder.go
│ │ ├── icon.go
│ │ ├── integration.go
│ │ ├── session.go
│ │ ├── storage.go
│ │ ├── user.go
│ │ ├── user_session.go
│ │ └── webauthn.go
│ ├── systemd/
│ │ └── systemd.go
│ ├── template/
│ │ ├── engine.go
│ │ ├── functions.go
│ │ ├── functions_test.go
│ │ └── templates/
│ │ ├── common/
│ │ │ ├── feed_list.html
│ │ │ ├── feed_menu.html
│ │ │ ├── item_meta.html
│ │ │ ├── layout.html
│ │ │ ├── pagination.html
│ │ │ └── settings_menu.html
│ │ └── views/
│ │ ├── about.html
│ │ ├── add_subscription.html
│ │ ├── api_keys.html
│ │ ├── categories.html
│ │ ├── category_entries.html
│ │ ├── category_feeds.html
│ │ ├── choose_subscription.html
│ │ ├── create_api_key.html
│ │ ├── create_category.html
│ │ ├── create_user.html
│ │ ├── edit_category.html
│ │ ├── edit_feed.html
│ │ ├── edit_user.html
│ │ ├── entry.html
│ │ ├── feed_entries.html
│ │ ├── feeds.html
│ │ ├── history_entries.html
│ │ ├── import.html
│ │ ├── integrations.html
│ │ ├── login.html
│ │ ├── offline.html
│ │ ├── search.html
│ │ ├── sessions.html
│ │ ├── settings.html
│ │ ├── shared_entries.html
│ │ ├── starred_entries.html
│ │ ├── tag_entries.html
│ │ ├── unread_entries.html
│ │ ├── users.html
│ │ └── webauthn_rename.html
│ ├── timezone/
│ │ ├── timezone.go
│ │ └── timezone_test.go
│ ├── ui/
│ │ ├── about.go
│ │ ├── api_key_create.go
│ │ ├── api_key_list.go
│ │ ├── api_key_remove.go
│ │ ├── api_key_save.go
│ │ ├── category_create.go
│ │ ├── category_edit.go
│ │ ├── category_entries.go
│ │ ├── category_entries_all.go
│ │ ├── category_entries_starred.go
│ │ ├── category_feeds.go
│ │ ├── category_list.go
│ │ ├── category_mark_as_read.go
│ │ ├── category_refresh.go
│ │ ├── category_remove.go
│ │ ├── category_remove_feed.go
│ │ ├── category_save.go
│ │ ├── category_update.go
│ │ ├── entry_category.go
│ │ ├── entry_enclosure_save_position.go
│ │ ├── entry_feed.go
│ │ ├── entry_read.go
│ │ ├── entry_save.go
│ │ ├── entry_scraper.go
│ │ ├── entry_search.go
│ │ ├── entry_starred.go
│ │ ├── entry_tag.go
│ │ ├── entry_toggle_starred.go
│ │ ├── entry_unread.go
│ │ ├── entry_update_status.go
│ │ ├── feed_edit.go
│ │ ├── feed_entries.go
│ │ ├── feed_entries_all.go
│ │ ├── feed_icon.go
│ │ ├── feed_list.go
│ │ ├── feed_mark_as_read.go
│ │ ├── feed_refresh.go
│ │ ├── feed_remove.go
│ │ ├── feed_update.go
│ │ ├── form/
│ │ │ ├── api_key.go
│ │ │ ├── auth.go
│ │ │ ├── category.go
│ │ │ ├── feed.go
│ │ │ ├── integration.go
│ │ │ ├── settings.go
│ │ │ ├── settings_test.go
│ │ │ ├── subscription.go
│ │ │ ├── user.go
│ │ │ └── webauthn.go
│ │ ├── handler.go
│ │ ├── history_entries.go
│ │ ├── history_flush.go
│ │ ├── integration_show.go
│ │ ├── integration_update.go
│ │ ├── login_check.go
│ │ ├── login_show.go
│ │ ├── logout.go
│ │ ├── middleware.go
│ │ ├── oauth2.go
│ │ ├── oauth2_callback.go
│ │ ├── oauth2_redirect.go
│ │ ├── oauth2_unlink.go
│ │ ├── offline.go
│ │ ├── opml_export.go
│ │ ├── opml_import.go
│ │ ├── opml_upload.go
│ │ ├── pagination.go
│ │ ├── proxy.go
│ │ ├── search.go
│ │ ├── session/
│ │ │ └── session.go
│ │ ├── session_list.go
│ │ ├── session_remove.go
│ │ ├── settings_show.go
│ │ ├── settings_update.go
│ │ ├── share.go
│ │ ├── shared_entries.go
│ │ ├── starred_entries.go
│ │ ├── starred_entry_category.go
│ │ ├── static/
│ │ │ ├── css/
│ │ │ │ ├── common.css
│ │ │ │ ├── dark.css
│ │ │ │ ├── light.css
│ │ │ │ ├── sans_serif.css
│ │ │ │ ├── serif.css
│ │ │ │ └── system.css
│ │ │ ├── js/
│ │ │ │ ├── .eslintrc.json
│ │ │ │ ├── .jshintrc
│ │ │ │ ├── app.js
│ │ │ │ ├── keyboard_handler.js
│ │ │ │ ├── service_worker.js
│ │ │ │ ├── touch_handler.js
│ │ │ │ └── webauthn_handler.js
│ │ │ └── static.go
│ │ ├── static_app_icon.go
│ │ ├── static_favicon.go
│ │ ├── static_javascript.go
│ │ ├── static_manifest.go
│ │ ├── static_stylesheet.go
│ │ ├── subscription_add.go
│ │ ├── subscription_bookmarklet.go
│ │ ├── subscription_choose.go
│ │ ├── subscription_submit.go
│ │ ├── tag_entries_all.go
│ │ ├── ui.go
│ │ ├── unread_entries.go
│ │ ├── unread_entry_category.go
│ │ ├── unread_entry_feed.go
│ │ ├── unread_mark_all_read.go
│ │ ├── user_create.go
│ │ ├── user_edit.go
│ │ ├── user_list.go
│ │ ├── user_remove.go
│ │ ├── user_save.go
│ │ ├── user_update.go
│ │ ├── view/
│ │ │ └── view.go
│ │ └── webauthn.go
│ ├── urllib/
│ │ ├── url.go
│ │ └── url_test.go
│ ├── validator/
│ │ ├── api_key.go
│ │ ├── category.go
│ │ ├── enclosure.go
│ │ ├── enclosure_test.go
│ │ ├── entry.go
│ │ ├── entry_test.go
│ │ ├── feed.go
│ │ ├── filter.go
│ │ ├── filter_test.go
│ │ ├── subscription.go
│ │ ├── subscription_test.go
│ │ ├── user.go
│ │ ├── user_test.go
│ │ ├── validator.go
│ │ └── validator_test.go
│ ├── version/
│ │ ├── version.go
│ │ └── version_test.go
│ └── worker/
│ ├── pool.go
│ └── worker.go
├── main.go
├── miniflux.1
└── packaging/
├── debian/
│ ├── Dockerfile
│ ├── build.sh
│ ├── compat
│ ├── control
│ ├── copyright
│ ├── miniflux.dirs
│ ├── miniflux.manpages
│ ├── miniflux.postinst
│ └── rules
├── docker/
│ ├── alpine/
│ │ └── Dockerfile
│ └── distroless/
│ └── Dockerfile
├── miniflux.conf
├── rpm/
│ ├── Dockerfile
│ └── miniflux.spec
└── systemd/
└── miniflux.service
Showing preview only (310K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (3065 symbols across 405 files)
FILE: .github/workflows/scripts/commit-checker.py
function get_commit_message (line 13) | def get_commit_message(commit_hash: str) -> str:
function check_commit_message (line 28) | def check_commit_message(message: str, pattern: str = CONVENTIONAL_COMMI...
function check_commit_range (line 35) | def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, s...
function main (line 61) | def main() -> None:
FILE: client/client.go
type Client (line 18) | type Client struct
method Healthcheck (line 63) | func (c *Client) Healthcheck() error {
method HealthcheckContext (line 70) | func (c *Client) HealthcheckContext(ctx context.Context) error {
method Version (line 90) | func (c *Client) Version() (*VersionResponse, error) {
method VersionContext (line 97) | func (c *Client) VersionContext(ctx context.Context) (*VersionResponse...
method Me (line 113) | func (c *Client) Me() (*User, error) {
method MeContext (line 120) | func (c *Client) MeContext(ctx context.Context) (*User, error) {
method Users (line 136) | func (c *Client) Users() (Users, error) {
method UsersContext (line 143) | func (c *Client) UsersContext(ctx context.Context) (Users, error) {
method UserByID (line 159) | func (c *Client) UserByID(userID int64) (*User, error) {
method UserByIDContext (line 166) | func (c *Client) UserByIDContext(ctx context.Context, userID int64) (*...
method UserByUsername (line 182) | func (c *Client) UserByUsername(username string) (*User, error) {
method UserByUsernameContext (line 189) | func (c *Client) UserByUsernameContext(ctx context.Context, username s...
method CreateUser (line 205) | func (c *Client) CreateUser(username, password string, isAdmin bool) (...
method CreateUserContext (line 212) | func (c *Client) CreateUserContext(ctx context.Context, username, pass...
method UpdateUser (line 232) | func (c *Client) UpdateUser(userID int64, userChanges *UserModificatio...
method UpdateUserContext (line 239) | func (c *Client) UpdateUserContext(ctx context.Context, userID int64, ...
method DeleteUser (line 255) | func (c *Client) DeleteUser(userID int64) error {
method DeleteUserContext (line 262) | func (c *Client) DeleteUserContext(ctx context.Context, userID int64) ...
method APIKeys (line 267) | func (c *Client) APIKeys() (APIKeys, error) {
method APIKeysContext (line 274) | func (c *Client) APIKeysContext(ctx context.Context) (APIKeys, error) {
method CreateAPIKey (line 290) | func (c *Client) CreateAPIKey(description string) (*APIKey, error) {
method CreateAPIKeyContext (line 297) | func (c *Client) CreateAPIKeyContext(ctx context.Context, description ...
method DeleteAPIKey (line 315) | func (c *Client) DeleteAPIKey(apiKeyID int64) error {
method DeleteAPIKeyContext (line 322) | func (c *Client) DeleteAPIKeyContext(ctx context.Context, apiKeyID int...
method MarkAllAsRead (line 327) | func (c *Client) MarkAllAsRead(userID int64) error {
method MarkAllAsReadContext (line 334) | func (c *Client) MarkAllAsReadContext(ctx context.Context, userID int6...
method IntegrationsStatus (line 340) | func (c *Client) IntegrationsStatus() (bool, error) {
method IntegrationsStatusContext (line 347) | func (c *Client) IntegrationsStatusContext(ctx context.Context) (bool,...
method Discover (line 366) | func (c *Client) Discover(url string) (Subscriptions, error) {
method DiscoverContext (line 373) | func (c *Client) DiscoverContext(ctx context.Context, url string) (Sub...
method Categories (line 389) | func (c *Client) Categories() (Categories, error) {
method CategoriesContext (line 396) | func (c *Client) CategoriesContext(ctx context.Context) (Categories, e...
method CategoriesWithCounters (line 412) | func (c *Client) CategoriesWithCounters() (Categories, error) {
method CategoriesWithCountersContext (line 419) | func (c *Client) CategoriesWithCountersContext(ctx context.Context) (C...
method CreateCategory (line 435) | func (c *Client) CreateCategory(title string) (*Category, error) {
method CreateCategoryContext (line 442) | func (c *Client) CreateCategoryContext(ctx context.Context, title stri...
method CreateCategoryWithOptions (line 460) | func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCrea...
method CreateCategoryWithOptionsContext (line 467) | func (c *Client) CreateCategoryWithOptionsContext(ctx context.Context,...
method UpdateCategory (line 482) | func (c *Client) UpdateCategory(categoryID int64, title string) (*Cate...
method UpdateCategoryContext (line 489) | func (c *Client) UpdateCategoryContext(ctx context.Context, categoryID...
method UpdateCategoryWithOptions (line 507) | func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryC...
method UpdateCategoryWithOptionsContext (line 514) | func (c *Client) UpdateCategoryWithOptionsContext(ctx context.Context,...
method MarkCategoryAsRead (line 530) | func (c *Client) MarkCategoryAsRead(categoryID int64) error {
method MarkCategoryAsReadContext (line 537) | func (c *Client) MarkCategoryAsReadContext(ctx context.Context, catego...
method CategoryFeeds (line 543) | func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
method CategoryFeedsContext (line 550) | func (c *Client) CategoryFeedsContext(ctx context.Context, categoryID ...
method DeleteCategory (line 566) | func (c *Client) DeleteCategory(categoryID int64) error {
method DeleteCategoryContext (line 573) | func (c *Client) DeleteCategoryContext(ctx context.Context, categoryID...
method RefreshCategory (line 578) | func (c *Client) RefreshCategory(categoryID int64) error {
method RefreshCategoryContext (line 585) | func (c *Client) RefreshCategoryContext(ctx context.Context, categoryI...
method Feeds (line 591) | func (c *Client) Feeds() (Feeds, error) {
method FeedsContext (line 598) | func (c *Client) FeedsContext(ctx context.Context) (Feeds, error) {
method Export (line 614) | func (c *Client) Export() ([]byte, error) {
method ExportContext (line 621) | func (c *Client) ExportContext(ctx context.Context) ([]byte, error) {
method Import (line 637) | func (c *Client) Import(f io.ReadCloser) error {
method ImportContext (line 644) | func (c *Client) ImportContext(ctx context.Context, f io.ReadCloser) e...
method Feed (line 650) | func (c *Client) Feed(feedID int64) (*Feed, error) {
method FeedContext (line 657) | func (c *Client) FeedContext(ctx context.Context, feedID int64) (*Feed...
method CreateFeed (line 673) | func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) ...
method CreateFeedContext (line 680) | func (c *Client) CreateFeedContext(ctx context.Context, feedCreationRe...
method UpdateFeed (line 700) | func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificatio...
method UpdateFeedContext (line 707) | func (c *Client) UpdateFeedContext(ctx context.Context, feedID int64, ...
method ImportFeedEntry (line 723) | func (c *Client) ImportFeedEntry(feedID int64, payload any) (int64, er...
method MarkFeedAsRead (line 749) | func (c *Client) MarkFeedAsRead(feedID int64) error {
method MarkFeedAsReadContext (line 756) | func (c *Client) MarkFeedAsReadContext(ctx context.Context, feedID int...
method RefreshAllFeeds (line 762) | func (c *Client) RefreshAllFeeds() error {
method RefreshAllFeedsContext (line 769) | func (c *Client) RefreshAllFeedsContext(ctx context.Context) error {
method RefreshFeed (line 775) | func (c *Client) RefreshFeed(feedID int64) error {
method RefreshFeedContext (line 782) | func (c *Client) RefreshFeedContext(ctx context.Context, feedID int64)...
method DeleteFeed (line 788) | func (c *Client) DeleteFeed(feedID int64) error {
method DeleteFeedContext (line 795) | func (c *Client) DeleteFeedContext(ctx context.Context, feedID int64) ...
method FeedIcon (line 800) | func (c *Client) FeedIcon(feedID int64) (*FeedIcon, error) {
method FeedIconContext (line 807) | func (c *Client) FeedIconContext(ctx context.Context, feedID int64) (*...
method FeedEntry (line 823) | func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
method FeedEntryContext (line 830) | func (c *Client) FeedEntryContext(ctx context.Context, feedID, entryID...
method CategoryEntry (line 846) | func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, err...
method CategoryEntryContext (line 853) | func (c *Client) CategoryEntryContext(ctx context.Context, categoryID,...
method Entry (line 869) | func (c *Client) Entry(entryID int64) (*Entry, error) {
method EntryContext (line 876) | func (c *Client) EntryContext(ctx context.Context, entryID int64) (*En...
method Entries (line 892) | func (c *Client) Entries(filter *Filter) (*EntryResultSet, error) {
method EntriesContext (line 899) | func (c *Client) EntriesContext(ctx context.Context, filter *Filter) (...
method FeedEntries (line 917) | func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResu...
method FeedEntriesContext (line 924) | func (c *Client) FeedEntriesContext(ctx context.Context, feedID int64,...
method CategoryEntries (line 942) | func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*E...
method CategoryEntriesContext (line 949) | func (c *Client) CategoryEntriesContext(ctx context.Context, categoryI...
method UpdateEntries (line 967) | func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
method UpdateEntriesContext (line 974) | func (c *Client) UpdateEntriesContext(ctx context.Context, entryIDs []...
method UpdateEntry (line 985) | func (c *Client) UpdateEntry(entryID int64, entryChanges *EntryModific...
method UpdateEntryContext (line 992) | func (c *Client) UpdateEntryContext(ctx context.Context, entryID int64...
method ToggleStarred (line 1008) | func (c *Client) ToggleStarred(entryID int64) error {
method ToggleStarredContext (line 1015) | func (c *Client) ToggleStarredContext(ctx context.Context, entryID int...
method SaveEntry (line 1021) | func (c *Client) SaveEntry(entryID int64) error {
method SaveEntryContext (line 1028) | func (c *Client) SaveEntryContext(ctx context.Context, entryID int64) ...
method FetchEntryOriginalContent (line 1034) | func (c *Client) FetchEntryOriginalContent(entryID int64) (string, err...
method FetchEntryOriginalContentContext (line 1041) | func (c *Client) FetchEntryOriginalContentContext(ctx context.Context,...
method FetchCounters (line 1060) | func (c *Client) FetchCounters() (*FeedCounters, error) {
method FetchCountersContext (line 1067) | func (c *Client) FetchCountersContext(ctx context.Context) (*FeedCount...
method FlushHistory (line 1083) | func (c *Client) FlushHistory() error {
method FlushHistoryContext (line 1090) | func (c *Client) FlushHistoryContext(ctx context.Context) error {
method Icon (line 1096) | func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
method IconContext (line 1103) | func (c *Client) IconContext(ctx context.Context, iconID int64) (*Feed...
method Enclosure (line 1119) | func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) {
method EnclosureContext (line 1126) | func (c *Client) EnclosureContext(ctx context.Context, enclosureID int...
method UpdateEnclosure (line 1142) | func (c *Client) UpdateEnclosure(enclosureID int64, enclosureUpdate *E...
method UpdateEnclosureContext (line 1149) | func (c *Client) UpdateEnclosureContext(ctx context.Context, enclosure...
function New (line 27) | func New(endpoint string, credentials ...string) *Client {
function NewClient (line 32) | func NewClient(endpoint string, credentials ...string) *Client {
function NewClientWithOptions (line 44) | func NewClientWithOptions(endpoint string, options ...Option) *Client {
function withDefaultTimeout (line 57) | func withDefaultTimeout() (context.Context, func()) {
function buildFilterQueryString (line 1154) | func buildFilterQueryString(path string, filter *Filter) string {
FILE: client/client_test.go
type roundTripperFunc (line 16) | type roundTripperFunc
method RoundTrip (line 18) | func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Respons...
function newFakeHTTPClient (line 22) | func newFakeHTTPClient(
function jsonResponseFrom (line 34) | func jsonResponseFrom(
function asJSON (line 52) | func asJSON(data any) string {
function expectRequest (line 60) | func expectRequest(
function expectFromJSON (line 80) | func expectFromJSON[T any](
function TestHealthcheck (line 95) | func TestHealthcheck(t *testing.T) {
function TestVersion (line 111) | func TestVersion(t *testing.T) {
function TestMe (line 139) | func TestMe(t *testing.T) {
function TestUsers (line 190) | func TestUsers(t *testing.T) {
function TestUserByID (line 219) | func TestUserByID(t *testing.T) {
function TestUserByUsername (line 242) | func TestUserByUsername(t *testing.T) {
function TestCreateUser (line 265) | func TestCreateUser(t *testing.T) {
function TestUpdateUser (line 301) | func TestUpdateUser(t *testing.T) {
function TestDeleteUser (line 333) | func TestDeleteUser(t *testing.T) {
function TestAPIKeys (line 346) | func TestAPIKeys(t *testing.T) {
function TestCreateAPIKey (line 375) | func TestCreateAPIKey(t *testing.T) {
function TestDeleteAPIKey (line 401) | func TestDeleteAPIKey(t *testing.T) {
function TestMarkAllAsRead (line 414) | func TestMarkAllAsRead(t *testing.T) {
function TestIntegrationsStatus (line 427) | func TestIntegrationsStatus(t *testing.T) {
function TestDiscover (line 448) | func TestDiscover(t *testing.T) {
function TestCategories (line 474) | func TestCategories(t *testing.T) {
function TestCategoriesWithCounters (line 497) | func TestCategoriesWithCounters(t *testing.T) {
function TestCreateCategory (line 524) | func TestCreateCategory(t *testing.T) {
function TestCreateCategoryWithOptions (line 549) | func TestCreateCategoryWithOptions(t *testing.T) {
function TestUpdateCategory (line 579) | func TestUpdateCategory(t *testing.T) {
function TestUpdateCategoryWithOptions (line 604) | func TestUpdateCategoryWithOptions(t *testing.T) {
function TestMarkCategoryAsRead (line 634) | func TestMarkCategoryAsRead(t *testing.T) {
function TestCategoryFeeds (line 647) | func TestCategoryFeeds(t *testing.T) {
function TestDeleteCategory (line 670) | func TestDeleteCategory(t *testing.T) {
function TestRefreshCategory (line 683) | func TestRefreshCategory(t *testing.T) {
function TestFeeds (line 696) | func TestFeeds(t *testing.T) {
function TestExport (line 750) | func TestExport(t *testing.T) {
function TestImport (line 772) | func TestImport(t *testing.T) {
function TestFeed (line 802) | func TestFeed(t *testing.T) {
function TestCreateFeed (line 823) | func TestCreateFeed(t *testing.T) {
function TestUpdateFeed (line 847) | func TestUpdateFeed(t *testing.T) {
function TestMarkFeedAsRead (line 870) | func TestMarkFeedAsRead(t *testing.T) {
function TestRefreshAllFeeds (line 883) | func TestRefreshAllFeeds(t *testing.T) {
function TestRefreshFeed (line 896) | func TestRefreshFeed(t *testing.T) {
function TestDeleteFeed (line 909) | func TestDeleteFeed(t *testing.T) {
function TestFeedIcon (line 922) | func TestFeedIcon(t *testing.T) {
function TestFeedEntry (line 944) | func TestFeedEntry(t *testing.T) {
function TestCategoryEntry (line 965) | func TestCategoryEntry(t *testing.T) {
function TestEntry (line 986) | func TestEntry(t *testing.T) {
function TestEntries (line 1007) | func TestEntries(t *testing.T) {
function TestFeedEntries (line 1034) | func TestFeedEntries(t *testing.T) {
function TestCategoryEntries (line 1062) | func TestCategoryEntries(t *testing.T) {
function TestUpdateEntries (line 1091) | func TestUpdateEntries(t *testing.T) {
function TestUpdateEntry (line 1111) | func TestUpdateEntry(t *testing.T) {
function TestToggleStarred (line 1137) | func TestToggleStarred(t *testing.T) {
function TestSaveEntry (line 1150) | func TestSaveEntry(t *testing.T) {
function TestFetchEntryOriginalContent (line 1163) | func TestFetchEntryOriginalContent(t *testing.T) {
function TestFetchCounters (line 1185) | func TestFetchCounters(t *testing.T) {
function TestFlushHistory (line 1210) | func TestFlushHistory(t *testing.T) {
function TestIcon (line 1223) | func TestIcon(t *testing.T) {
function TestEnclosure (line 1245) | func TestEnclosure(t *testing.T) {
function TestUpdateEnclosure (line 1267) | func TestUpdateEnclosure(t *testing.T) {
FILE: client/model.go
constant EntryStatusUnread (line 13) | EntryStatusUnread = "unread"
constant EntryStatusRead (line 14) | EntryStatusRead = "read"
constant EntryStatusRemoved (line 15) | EntryStatusRemoved = "removed"
type User (line 19) | type User struct
method String (line 53) | func (u User) String() string {
type UserCreationRequest (line 58) | type UserCreationRequest struct
type UserModificationRequest (line 67) | type UserModificationRequest struct
type Users (line 100) | type Users
type Category (line 103) | type Category struct
method String (line 112) | func (c Category) String() string {
type Categories (line 117) | type Categories
type CategoryCreationRequest (line 120) | type CategoryCreationRequest struct
type CategoryModificationRequest (line 126) | type CategoryModificationRequest struct
type Subscription (line 132) | type Subscription struct
method String (line 138) | func (s Subscription) String() string {
type Subscriptions (line 143) | type Subscriptions
type Feed (line 146) | type Feed struct
type FeedCreationRequest (line 181) | type FeedCreationRequest struct
type FeedModificationRequest (line 207) | type FeedModificationRequest struct
type FeedIcon (line 235) | type FeedIcon struct
type FeedCounters (line 241) | type FeedCounters struct
type Feeds (line 247) | type Feeds
type Entry (line 250) | type Entry struct
type EntryModificationRequest (line 273) | type EntryModificationRequest struct
type Entries (line 279) | type Entries
type Enclosure (line 282) | type Enclosure struct
type EnclosureUpdateRequest (line 292) | type EnclosureUpdateRequest struct
type Enclosures (line 297) | type Enclosures
constant FilterNotStarred (line 300) | FilterNotStarred = "0"
constant FilterOnlyStarred (line 301) | FilterOnlyStarred = "1"
type Filter (line 305) | type Filter struct
type EntryResultSet (line 328) | type EntryResultSet struct
type VersionResponse (line 334) | type VersionResponse struct
type APIKey (line 345) | type APIKey struct
type APIKeys (line 355) | type APIKeys
type APIKeyCreationRequest (line 358) | type APIKeyCreationRequest struct
function SetOptionalField (line 365) | func SetOptionalField[T any](value T) *T {
FILE: client/options.go
type Option (line 8) | type Option
function WithAPIKey (line 11) | func WithAPIKey(apiKey string) Option {
function WithCredentials (line 18) | func WithCredentials(username, password string) Option {
function WithHTTPClient (line 26) | func WithHTTPClient(client *http.Client) Option {
FILE: client/request.go
constant userAgent (line 20) | userAgent = "Miniflux Client Library"
constant defaultTimeout (line 21) | defaultTimeout = 80 * time.Second
type errorResponse (line 34) | type errorResponse struct
type request (line 38) | type request struct
method Get (line 46) | func (r *request) Get(ctx context.Context, path string) (io.ReadCloser...
method Post (line 50) | func (r *request) Post(ctx context.Context, path string, data any) (io...
method PostFile (line 54) | func (r *request) PostFile(ctx context.Context, path string, f io.Read...
method Put (line 58) | func (r *request) Put(ctx context.Context, path string, data any) (io....
method Delete (line 62) | func (r *request) Delete(ctx context.Context, path string) error {
method execute (line 67) | func (r *request) execute(
method buildHeaders (line 154) | func (r *request) buildHeaders() http.Header {
method toJSON (line 165) | func (r *request) toJSON(v any) []byte {
FILE: internal/api/api.go
type handler (line 13) | type handler struct
function NewHandler (line 20) | func NewHandler(store *storage.Storage, pool *worker.Pool) http.Handler {
FILE: internal/api/api_integration_test.go
constant skipIntegrationTestsMessage (line 20) | skipIntegrationTestsMessage = `Set TEST_MINIFLUX_* environment variables...
type integrationTestConfig (line 22) | type integrationTestConfig struct
method isConfigured (line 56) | func (c *integrationTestConfig) isConfigured() bool {
method genRandomUsername (line 60) | func (c *integrationTestConfig) genRandomUsername() string {
function newIntegrationTestConfig (line 34) | func newIntegrationTestConfig() *integrationTestConfig {
function TestIncorrectEndpoint (line 64) | func TestIncorrectEndpoint(t *testing.T) {
function TestHealthcheckEndpoint (line 81) | func TestHealthcheckEndpoint(t *testing.T) {
function TestVersionEndpoint (line 93) | func TestVersionEndpoint(t *testing.T) {
function TestInvalidCredentials (line 134) | func TestInvalidCredentials(t *testing.T) {
function TestGetMeEndpoint (line 151) | func TestGetMeEndpoint(t *testing.T) {
function TestGetUsersEndpointAsAdmin (line 168) | func TestGetUsersEndpointAsAdmin(t *testing.T) {
function TestGetUsersEndpointAsRegularUser (line 233) | func TestGetUsersEndpointAsRegularUser(t *testing.T) {
function TestCreateUserEndpointAsAdmin (line 253) | func TestCreateUserEndpointAsAdmin(t *testing.T) {
function TestCreateUserEndpointAsRegularUser (line 313) | func TestCreateUserEndpointAsRegularUser(t *testing.T) {
function TestCannotCreateDuplicateUser (line 333) | func TestCannotCreateDuplicateUser(t *testing.T) {
function TestRemoveUserEndpointAsAdmin (line 346) | func TestRemoveUserEndpointAsAdmin(t *testing.T) {
function TestRemoveUserEndpointAsRegularUser (line 363) | func TestRemoveUserEndpointAsRegularUser(t *testing.T) {
function TestGetUserByIDEndpointAsAdmin (line 383) | func TestGetUserByIDEndpointAsAdmin(t *testing.T) {
function TestGetUserByIDEndpointAsRegularUser (line 457) | func TestGetUserByIDEndpointAsRegularUser(t *testing.T) {
function TestGetUserByUsernameEndpointAsAdmin (line 477) | func TestGetUserByUsernameEndpointAsAdmin(t *testing.T) {
function TestGetUserByUsernameEndpointAsRegularUser (line 547) | func TestGetUserByUsernameEndpointAsRegularUser(t *testing.T) {
function TestUpdateUserEndpointByChangingDefaultTheme (line 567) | func TestUpdateUserEndpointByChangingDefaultTheme(t *testing.T) {
function TestUpdateUserEndpointByChangingExternalFonts (line 596) | func TestUpdateUserEndpointByChangingExternalFonts(t *testing.T) {
function TestUpdateUserEndpointByChangingExternalFontsWithInvalidValue (line 625) | func TestUpdateUserEndpointByChangingExternalFontsWithInvalidValue(t *te...
function TestUpdateUserEndpointByChangingCustomJS (line 649) | func TestUpdateUserEndpointByChangingCustomJS(t *testing.T) {
function TestUpdateUserEndpointByChangingDefaultThemeToInvalidValue (line 678) | func TestUpdateUserEndpointByChangingDefaultThemeToInvalidValue(t *testi...
function TestRegularUsersCannotUpdateOtherUsers (line 703) | func TestRegularUsersCannotUpdateOtherUsers(t *testing.T) {
function TestAPIKeysEndpoint (line 733) | func TestAPIKeysEndpoint(t *testing.T) {
function TestMarkUserAsReadEndpoint (line 843) | func TestMarkUserAsReadEndpoint(t *testing.T) {
function TestCannotMarkUserAsReadAsOtherUser (line 880) | func TestCannotMarkUserAsReadAsOtherUser(t *testing.T) {
function TestCreateCategoryEndpoint (line 905) | func TestCreateCategoryEndpoint(t *testing.T) {
function TestCreateCategoryWithEmptyTitle (line 944) | func TestCreateCategoryWithEmptyTitle(t *testing.T) {
function TestCannotCreateDuplicatedCategory (line 957) | func TestCannotCreateDuplicatedCategory(t *testing.T) {
function TestCreateCategoryWithOptions (line 983) | func TestCreateCategoryWithOptions(t *testing.T) {
function TestUpdateCategoryEndpoint (line 1025) | func TestUpdateCategoryEndpoint(t *testing.T) {
function TestUpdateCategoryWithOptions (line 1069) | func TestUpdateCategoryWithOptions(t *testing.T) {
function TestUpdateInexistingCategory (line 1150) | func TestUpdateInexistingCategory(t *testing.T) {
function TestDeleteCategoryEndpoint (line 1162) | func TestDeleteCategoryEndpoint(t *testing.T) {
function TestCannotDeleteInexistingCategory (line 1189) | func TestCannotDeleteInexistingCategory(t *testing.T) {
function TestCannotDeleteCategoryOfAnotherUser (line 1202) | func TestCannotDeleteCategoryOfAnotherUser(t *testing.T) {
function TestGetCategoriesEndpoint (line 1228) | func TestGetCategoriesEndpoint(t *testing.T) {
function TestMarkCategoryAsReadEndpoint (line 1321) | func TestMarkCategoryAsReadEndpoint(t *testing.T) {
function TestCreateFeedEndpoint (line 1365) | func TestCreateFeedEndpoint(t *testing.T) {
function TestCannotCreateDuplicatedFeed (line 1398) | func TestCannotCreateDuplicatedFeed(t *testing.T) {
function TestCreateFeedWithInexistingCategory (line 1433) | func TestCreateFeedWithInexistingCategory(t *testing.T) {
function TestCreateFeedWithEmptyFeedURL (line 1459) | func TestCreateFeedWithEmptyFeedURL(t *testing.T) {
function TestCreateFeedWithInvalidFeedURL (line 1474) | func TestCreateFeedWithInvalidFeedURL(t *testing.T) {
function TestCreateDisabledFeed (line 1489) | func TestCreateDisabledFeed(t *testing.T) {
function TestCreateFeedWithDisabledHTTPCache (line 1523) | func TestCreateFeedWithDisabledHTTPCache(t *testing.T) {
function TestCreateFeedWithScraperRule (line 1557) | func TestCreateFeedWithScraperRule(t *testing.T) {
function TestUpdateFeedEndpoint (line 1591) | func TestUpdateFeedEndpoint(t *testing.T) {
function TestCannotHaveDuplicateFeedWhenUpdatingFeed (line 1628) | func TestCannotHaveDuplicateFeedWhenUpdatingFeed(t *testing.T) {
function TestUpdateFeedWithInvalidCategory (line 1664) | func TestUpdateFeedWithInvalidCategory(t *testing.T) {
function TestMarkFeedAsReadEndpoint (line 1696) | func TestMarkFeedAsReadEndpoint(t *testing.T) {
function TestFetchCountersEndpoint (line 1735) | func TestFetchCountersEndpoint(t *testing.T) {
function TestDeleteFeedEndpoint (line 1772) | func TestDeleteFeedEndpoint(t *testing.T) {
function TestRefreshAllFeedsEndpoint (line 1800) | func TestRefreshAllFeedsEndpoint(t *testing.T) {
function TestRefreshFeedEndpoint (line 1821) | func TestRefreshFeedEndpoint(t *testing.T) {
function TestGetFeedEndpoint (line 1849) | func TestGetFeedEndpoint(t *testing.T) {
function TestGetFeedIcon (line 1894) | func TestGetFeedIcon(t *testing.T) {
function TestGetFeedIconWithInexistingFeedID (line 1952) | func TestGetFeedIconWithInexistingFeedID(t *testing.T) {
function TestGetFeedsEndpoint (line 1965) | func TestGetFeedsEndpoint(t *testing.T) {
function TestGetCategoryFeedsEndpoint (line 2006) | func TestGetCategoryFeedsEndpoint(t *testing.T) {
function TestExportEndpoint (line 2053) | func TestExportEndpoint(t *testing.T) {
function TestImportEndpoint (line 2087) | func TestImportEndpoint(t *testing.T) {
function TestDiscoverSubscriptionsEndpoint (line 2118) | func TestDiscoverSubscriptionsEndpoint(t *testing.T) {
function TestDiscoverSubscriptionsWithInvalidURL (line 2143) | func TestDiscoverSubscriptionsWithInvalidURL(t *testing.T) {
function TestDiscoverSubscriptionsWithNoSubscription (line 2156) | func TestDiscoverSubscriptionsWithNoSubscription(t *testing.T) {
function TestGetAllFeedEntriesEndpoint (line 2168) | func TestGetAllFeedEntriesEndpoint(t *testing.T) {
function TestGetAllCategoryEntriesEndpoint (line 2217) | func TestGetAllCategoryEntriesEndpoint(t *testing.T) {
function TestGetAllEntriesEndpointWithFilter (line 2272) | func TestGetAllEntriesEndpointWithFilter(t *testing.T) {
function TestGetGlobalEntriesEndpoint (line 2359) | func TestGetGlobalEntriesEndpoint(t *testing.T) {
function TestCannotGetRemovedEntries (line 2416) | func TestCannotGetRemovedEntries(t *testing.T) {
function TestUpdateEnclosureEndpoint (line 2474) | func TestUpdateEnclosureEndpoint(t *testing.T) {
function TestGetEnclosureEndpoint (line 2532) | func TestGetEnclosureEndpoint(t *testing.T) {
function TestGetEntryEndpoints (line 2586) | func TestGetEntryEndpoints(t *testing.T) {
function TestUpdateEntryStatusEndpoint (line 2650) | func TestUpdateEntryStatusEndpoint(t *testing.T) {
function TestUpdateEntryEndpoint (line 2692) | func TestUpdateEntryEndpoint(t *testing.T) {
function TestToggleStarredEndpoint (line 2752) | func TestToggleStarredEndpoint(t *testing.T) {
function TestSaveEntryEndpoint (line 2794) | func TestSaveEntryEndpoint(t *testing.T) {
function TestFetchIntegrationsStatusEndpoint (line 2827) | func TestFetchIntegrationsStatusEndpoint(t *testing.T) {
function TestFetchContentEndpoint (line 2853) | func TestFetchContentEndpoint(t *testing.T) {
function TestFlushHistoryEndpoint (line 2891) | func TestFlushHistoryEndpoint(t *testing.T) {
function TestImportFeedEntryEndpoint (line 2937) | func TestImportFeedEntryEndpoint(t *testing.T) {
FILE: internal/api/api_key_handlers.go
method createAPIKeyHandler (line 18) | func (h *handler) createAPIKeyHandler(w http.ResponseWriter, r *http.Req...
method getAPIKeysHandler (line 41) | func (h *handler) getAPIKeysHandler(w http.ResponseWriter, r *http.Reque...
method deleteAPIKeyHandler (line 51) | func (h *handler) deleteAPIKeyHandler(w http.ResponseWriter, r *http.Req...
FILE: internal/api/api_test.go
function TestNewHandlerHandlesOptionsRequests (line 16) | func TestNewHandlerHandlesOptionsRequests(t *testing.T) {
function TestVersionHandler (line 45) | func TestVersionHandler(t *testing.T) {
function TestNewHandlerSupportsBasePathStripping (line 94) | func TestNewHandlerSupportsBasePathStripping(t *testing.T) {
FILE: internal/api/category_handlers.go
method createCategoryHandler (line 20) | func (h *handler) createCategoryHandler(w http.ResponseWriter, r *http.R...
method updateCategoryHandler (line 43) | func (h *handler) updateCategoryHandler(w http.ResponseWriter, r *http.R...
method markCategoryAsReadHandler (line 84) | func (h *handler) markCategoryAsReadHandler(w http.ResponseWriter, r *ht...
method getCategoriesHandler (line 112) | func (h *handler) getCategoriesHandler(w http.ResponseWriter, r *http.Re...
method removeCategoryHandler (line 130) | func (h *handler) removeCategoryHandler(w http.ResponseWriter, r *http.R...
method refreshCategoryHandler (line 152) | func (h *handler) refreshCategoryHandler(w http.ResponseWriter, r *http....
FILE: internal/api/enclosure_handlers.go
method getEnclosureByIDHandler (line 18) | func (h *handler) getEnclosureByIDHandler(w http.ResponseWriter, r *http...
method updateEnclosureByIDHandler (line 47) | func (h *handler) updateEnclosureByIDHandler(w http.ResponseWriter, r *h...
FILE: internal/api/entry_handlers.go
method getEntryFromBuilder (line 27) | func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Req...
method getFeedEntryHandler (line 45) | func (h *handler) getFeedEntryHandler(w http.ResponseWriter, r *http.Req...
method getCategoryEntryHandler (line 66) | func (h *handler) getCategoryEntryHandler(w http.ResponseWriter, r *http...
method getEntryHandler (line 87) | func (h *handler) getEntryHandler(w http.ResponseWriter, r *http.Request) {
method getFeedEntriesHandler (line 101) | func (h *handler) getFeedEntriesHandler(w http.ResponseWriter, r *http.R...
method getCategoryEntriesHandler (line 111) | func (h *handler) getCategoryEntriesHandler(w http.ResponseWriter, r *ht...
method getEntriesHandler (line 120) | func (h *handler) getEntriesHandler(w http.ResponseWriter, r *http.Reque...
method findEntries (line 124) | func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, fe...
method setEntryStatusHandler (line 207) | func (h *handler) setEntryStatusHandler(w http.ResponseWriter, r *http.R...
method toggleStarredHandler (line 227) | func (h *handler) toggleStarredHandler(w http.ResponseWriter, r *http.Re...
method saveEntryHandler (line 242) | func (h *handler) saveEntryHandler(w http.ResponseWriter, r *http.Reques...
method updateEntryHandler (line 280) | func (h *handler) updateEntryHandler(w http.ResponseWriter, r *http.Requ...
method importFeedEntryHandler (line 343) | func (h *handler) importFeedEntryHandler(w http.ResponseWriter, r *http....
method fetchContentHandler (line 447) | func (h *handler) fetchContentHandler(w http.ResponseWriter, r *http.Req...
method flushHistoryHandler (line 511) | func (h *handler) flushHistoryHandler(w http.ResponseWriter, r *http.Req...
function configureFilters (line 517) | func configureFilters(builder *storage.EntryQueryBuilder, r *http.Reques...
FILE: internal/api/feed_handlers.go
method createFeedHandler (line 21) | func (h *handler) createFeedHandler(w http.ResponseWriter, r *http.Reque...
method refreshFeedHandler (line 54) | func (h *handler) refreshFeedHandler(w http.ResponseWriter, r *http.Requ...
method refreshAllFeedsHandler (line 76) | func (h *handler) refreshAllFeedsHandler(w http.ResponseWriter, r *http....
method updateFeedHandler (line 103) | func (h *handler) updateFeedHandler(w http.ResponseWriter, r *http.Reque...
method markFeedAsReadHandler (line 149) | func (h *handler) markFeedAsReadHandler(w http.ResponseWriter, r *http.R...
method getCategoryFeedsHandler (line 171) | func (h *handler) getCategoryFeedsHandler(w http.ResponseWriter, r *http...
method getFeedsHandler (line 200) | func (h *handler) getFeedsHandler(w http.ResponseWriter, r *http.Request) {
method fetchCountersHandler (line 210) | func (h *handler) fetchCountersHandler(w http.ResponseWriter, r *http.Re...
method getFeedHandler (line 220) | func (h *handler) getFeedHandler(w http.ResponseWriter, r *http.Request) {
method removeFeedHandler (line 241) | func (h *handler) removeFeedHandler(w http.ResponseWriter, r *http.Reque...
FILE: internal/api/icon_handlers.go
method getIconByFeedIDHandler (line 14) | func (h *handler) getIconByFeedIDHandler(w http.ResponseWriter, r *http....
method getIconByIconIDHandler (line 39) | func (h *handler) getIconByIconIDHandler(w http.ResponseWriter, r *http....
FILE: internal/api/messages.go
type feedIconResponse (line 10) | type feedIconResponse struct
type entriesResponse (line 16) | type entriesResponse struct
type integrationsStatusResponse (line 21) | type integrationsStatusResponse struct
type entryIDResponse (line 25) | type entryIDResponse struct
type entryContentResponse (line 29) | type entryContentResponse struct
type entryImportRequest (line 34) | type entryImportRequest struct
type feedCreationResponse (line 47) | type feedCreationResponse struct
type importFeedsResponse (line 51) | type importFeedsResponse struct
type versionResponse (line 55) | type versionResponse struct
FILE: internal/api/middleware.go
type middleware (line 16) | type middleware struct
method withCORSHeaders (line 23) | func (m *middleware) withCORSHeaders(next http.Handler) http.Handler {
method validateAPIKeyAuth (line 37) | func (m *middleware) validateAPIKeyAuth(next http.Handler) http.Handler {
method validateBasicAuth (line 90) | func (m *middleware) validateBasicAuth(next http.Handler) http.Handler {
function newMiddleware (line 20) | func newMiddleware(s *storage.Storage) *middleware {
FILE: internal/api/opml_handlers.go
method exportFeedsHandler (line 14) | func (h *handler) exportFeedsHandler(w http.ResponseWriter, r *http.Requ...
method importFeedsHandler (line 25) | func (h *handler) importFeedsHandler(w http.ResponseWriter, r *http.Requ...
FILE: internal/api/subscription_handlers.go
method discoverSubscriptionsHandler (line 20) | func (h *handler) discoverSubscriptionsHandler(w http.ResponseWriter, r ...
FILE: internal/api/user_handlers.go
method currentUserHandler (line 17) | func (h *handler) currentUserHandler(w http.ResponseWriter, r *http.Requ...
method createUserHandler (line 27) | func (h *handler) createUserHandler(w http.ResponseWriter, r *http.Reque...
method updateUserHandler (line 53) | func (h *handler) updateUserHandler(w http.ResponseWriter, r *http.Reque...
method markUserAsReadHandler (line 103) | func (h *handler) markUserAsReadHandler(w http.ResponseWriter, r *http.R...
method getIntegrationsStatusHandler (line 128) | func (h *handler) getIntegrationsStatusHandler(w http.ResponseWriter, r ...
method usersHandler (line 140) | func (h *handler) usersHandler(w http.ResponseWriter, r *http.Request) {
method dispatchUserLookupHandler (line 156) | func (h *handler) dispatchUserLookupHandler(w http.ResponseWriter, r *ht...
method userByIDHandler (line 169) | func (h *handler) userByIDHandler(w http.ResponseWriter, r *http.Request) {
method userByUsernameHandler (line 196) | func (h *handler) userByUsernameHandler(w http.ResponseWriter, r *http.R...
method removeUserHandler (line 217) | func (h *handler) removeUserHandler(w http.ResponseWriter, r *http.Reque...
FILE: internal/api/version_handler.go
method versionHandler (line 14) | func (h *handler) versionHandler(w http.ResponseWriter, r *http.Request) {
FILE: internal/cli/ask_credentials.go
function askCredentials (line 16) | func askCredentials() (string, string) {
FILE: internal/cli/cleanup_tasks.go
function runCleanupTasks (line 16) | func runCleanupTasks(store *storage.Storage) {
FILE: internal/cli/cli.go
constant flagInfoHelp (line 23) | flagInfoHelp = "Show build information"
constant flagVersionHelp (line 24) | flagVersionHelp = "Show application version"
constant flagMigrateHelp (line 25) | flagMigrateHelp = "Run SQL migrations"
constant flagFlushSessionsHelp (line 26) | flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
constant flagCreateAdminHelp (line 27) | flagCreateAdminHelp = "Create an admin user from an interactive ter...
constant flagResetPasswordHelp (line 28) | flagResetPasswordHelp = "Reset user password"
constant flagResetFeedErrorsHelp (line 29) | flagResetFeedErrorsHelp = "Clear all feed errors for all users"
constant flagDebugModeHelp (line 30) | flagDebugModeHelp = "Show debug logs"
constant flagConfigFileHelp (line 31) | flagConfigFileHelp = "Load configuration file"
constant flagConfigDumpHelp (line 32) | flagConfigDumpHelp = "Print parsed configuration values"
constant flagHealthCheckHelp (line 33) | flagHealthCheckHelp = `Perform a health check on the given endpoint...
constant flagRefreshFeedsHelp (line 34) | flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
constant flagRunCleanupTasksHelp (line 35) | flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and a...
constant flagExportUserFeedsHelp (line 36) | flagExportUserFeedsHelp = "Export user feeds (provide the username as a...
constant flagResetNextCheckAtHelp (line 37) | flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
function Parse (line 41) | func Parse() {
function printErrorAndExit (line 271) | func printErrorAndExit(err error) {
FILE: internal/cli/create_admin.go
function createAdminUserFromEnvironmentVariables (line 15) | func createAdminUserFromEnvironmentVariables(store *storage.Storage) {
function createAdminUserFromInteractiveTerminal (line 19) | func createAdminUserFromInteractiveTerminal(store *storage.Storage) {
function createAdminUser (line 24) | func createAdminUser(store *storage.Storage, username, password string) {
FILE: internal/cli/daemon.go
function startDaemon (line 23) | func startDaemon(store *storage.Storage) {
FILE: internal/cli/export_feeds.go
function exportUserFeeds (line 13) | func exportUserFeeds(store *storage.Storage, username string) {
FILE: internal/cli/flush_sessions.go
function flushSessions (line 12) | func flushSessions(store *storage.Storage) {
FILE: internal/cli/health_check.go
function doHealthCheck (line 15) | func doHealthCheck(healthCheckEndpoint string) {
FILE: internal/cli/info.go
function info (line 13) | func info() {
FILE: internal/cli/logger.go
function InitializeDefaultLogger (line 11) | func InitializeDefaultLogger(logLevel string, logFile io.Writer, logForm...
FILE: internal/cli/refresh_feeds.go
function refreshFeeds (line 17) | func refreshFeeds(store *storage.Storage) {
FILE: internal/cli/reset_password.go
function resetPassword (line 15) | func resetPassword(store *storage.Storage) {
FILE: internal/cli/scheduler.go
function runScheduler (line 15) | func runScheduler(store *storage.Storage, pool *worker.Pool) {
function feedScheduler (line 33) | func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency ...
function cleanupScheduler (line 52) | func cleanupScheduler(store *storage.Storage, frequency time.Duration) {
FILE: internal/config/options.go
type optionPair (line 15) | type optionPair struct
type configValueType (line 20) | type configValueType
constant stringType (line 23) | stringType configValueType = iota
constant stringListType (line 24) | stringListType
constant boolType (line 25) | boolType
constant intType (line 26) | intType
constant int64Type (line 27) | int64Type
constant urlType (line 28) | urlType
constant secondType (line 29) | secondType
constant minuteType (line 30) | minuteType
constant hourType (line 31) | hourType
constant dayType (line 32) | dayType
constant secretFileType (line 33) | secretFileType
constant bytesType (line 34) | bytesType
type configValue (line 37) | type configValue struct
type configOptions (line 55) | type configOptions struct
method AdminPassword (line 611) | func (c *configOptions) AdminPassword() string {
method AdminUsername (line 615) | func (c *configOptions) AdminUsername() string {
method AuthProxyHeader (line 619) | func (c *configOptions) AuthProxyHeader() string {
method AuthProxyUserCreation (line 623) | func (c *configOptions) AuthProxyUserCreation() bool {
method BasePath (line 627) | func (c *configOptions) BasePath() string {
method BaseURL (line 631) | func (c *configOptions) BaseURL() string {
method RootURL (line 635) | func (c *configOptions) RootURL() string {
method BatchSize (line 639) | func (c *configOptions) BatchSize() int {
method CertDomain (line 643) | func (c *configOptions) CertDomain() string {
method CertFile (line 647) | func (c *configOptions) CertFile() string {
method CleanupArchiveBatchSize (line 651) | func (c *configOptions) CleanupArchiveBatchSize() int {
method CleanupArchiveReadInterval (line 655) | func (c *configOptions) CleanupArchiveReadInterval() time.Duration {
method CleanupArchiveUnreadInterval (line 659) | func (c *configOptions) CleanupArchiveUnreadInterval() time.Duration {
method CleanupFrequency (line 663) | func (c *configOptions) CleanupFrequency() time.Duration {
method CleanupRemoveSessionsInterval (line 667) | func (c *configOptions) CleanupRemoveSessionsInterval() time.Duration {
method CreateAdmin (line 671) | func (c *configOptions) CreateAdmin() bool {
method DatabaseConnectionLifetime (line 675) | func (c *configOptions) DatabaseConnectionLifetime() time.Duration {
method DatabaseMaxConns (line 679) | func (c *configOptions) DatabaseMaxConns() int {
method DatabaseMinConns (line 683) | func (c *configOptions) DatabaseMinConns() int {
method DatabaseURL (line 687) | func (c *configOptions) DatabaseURL() string {
method DisableHSTS (line 691) | func (c *configOptions) DisableHSTS() bool {
method DisableHTTPService (line 695) | func (c *configOptions) DisableHTTPService() bool {
method DisableLocalAuth (line 699) | func (c *configOptions) DisableLocalAuth() bool {
method DisableSchedulerService (line 703) | func (c *configOptions) DisableSchedulerService() bool {
method FetchBilibiliWatchTime (line 707) | func (c *configOptions) FetchBilibiliWatchTime() bool {
method FetchNebulaWatchTime (line 711) | func (c *configOptions) FetchNebulaWatchTime() bool {
method FetchOdyseeWatchTime (line 715) | func (c *configOptions) FetchOdyseeWatchTime() bool {
method FetchYouTubeWatchTime (line 719) | func (c *configOptions) FetchYouTubeWatchTime() bool {
method ForceRefreshInterval (line 723) | func (c *configOptions) ForceRefreshInterval() time.Duration {
method HasHTTPClientProxiesConfigured (line 727) | func (c *configOptions) HasHTTPClientProxiesConfigured() bool {
method HasAPI (line 731) | func (c *configOptions) HasAPI() bool {
method HasHTTPService (line 735) | func (c *configOptions) HasHTTPService() bool {
method HasHSTS (line 739) | func (c *configOptions) HasHSTS() bool {
method HasHTTPClientProxyURLConfigured (line 743) | func (c *configOptions) HasHTTPClientProxyURLConfigured() bool {
method HasMaintenanceMode (line 747) | func (c *configOptions) HasMaintenanceMode() bool {
method HasMetricsCollector (line 751) | func (c *configOptions) HasMetricsCollector() bool {
method HasSchedulerService (line 755) | func (c *configOptions) HasSchedulerService() bool {
method HasWatchdog (line 759) | func (c *configOptions) HasWatchdog() bool {
method HTTPClientMaxBodySize (line 763) | func (c *configOptions) HTTPClientMaxBodySize() int64 {
method HTTPClientProxies (line 767) | func (c *configOptions) HTTPClientProxies() []string {
method HTTPClientProxyURL (line 771) | func (c *configOptions) HTTPClientProxyURL() *url.URL {
method HTTPClientTimeout (line 775) | func (c *configOptions) HTTPClientTimeout() time.Duration {
method HTTPClientUserAgent (line 779) | func (c *configOptions) HTTPClientUserAgent() string {
method HTTPServerTimeout (line 786) | func (c *configOptions) HTTPServerTimeout() time.Duration {
method HTTPS (line 790) | func (c *configOptions) HTTPS() bool {
method FetcherAllowPrivateNetworks (line 794) | func (c *configOptions) FetcherAllowPrivateNetworks() bool {
method IntegrationAllowPrivateNetworks (line 798) | func (c *configOptions) IntegrationAllowPrivateNetworks() bool {
method InvidiousInstance (line 805) | func (c *configOptions) InvidiousInstance() string {
method IsAuthProxyUserCreationAllowed (line 809) | func (c *configOptions) IsAuthProxyUserCreationAllowed() bool {
method IsDefaultDatabaseURL (line 813) | func (c *configOptions) IsDefaultDatabaseURL() bool {
method IsOAuth2UserCreationAllowed (line 817) | func (c *configOptions) IsOAuth2UserCreationAllowed() bool {
method CertKeyFile (line 821) | func (c *configOptions) CertKeyFile() string {
method ListenAddr (line 825) | func (c *configOptions) ListenAddr() []string {
method LogFile (line 829) | func (c *configOptions) LogFile() string {
method LogDateTime (line 833) | func (c *configOptions) LogDateTime() bool {
method LogFormat (line 837) | func (c *configOptions) LogFormat() string {
method LogLevel (line 841) | func (c *configOptions) LogLevel() string {
method MaintenanceMessage (line 845) | func (c *configOptions) MaintenanceMessage() string {
method MaintenanceMode (line 849) | func (c *configOptions) MaintenanceMode() bool {
method MediaCustomProxyURL (line 853) | func (c *configOptions) MediaCustomProxyURL() *url.URL {
method MediaProxyHTTPClientTimeout (line 857) | func (c *configOptions) MediaProxyHTTPClientTimeout() time.Duration {
method MediaProxyMode (line 861) | func (c *configOptions) MediaProxyMode() string {
method MediaProxyPrivateKey (line 865) | func (c *configOptions) MediaProxyPrivateKey() []byte {
method MediaProxyResourceTypes (line 869) | func (c *configOptions) MediaProxyResourceTypes() []string {
method MetricsAllowedNetworks (line 873) | func (c *configOptions) MetricsAllowedNetworks() []string {
method MetricsCollector (line 877) | func (c *configOptions) MetricsCollector() bool {
method MetricsPassword (line 881) | func (c *configOptions) MetricsPassword() string {
method MetricsRefreshInterval (line 885) | func (c *configOptions) MetricsRefreshInterval() time.Duration {
method MetricsUsername (line 889) | func (c *configOptions) MetricsUsername() string {
method OAuth2ClientID (line 893) | func (c *configOptions) OAuth2ClientID() string {
method OAuth2ClientSecret (line 897) | func (c *configOptions) OAuth2ClientSecret() string {
method OAuth2OIDCDiscoveryEndpoint (line 901) | func (c *configOptions) OAuth2OIDCDiscoveryEndpoint() string {
method OAuth2OIDCProviderName (line 905) | func (c *configOptions) OAuth2OIDCProviderName() string {
method OAuth2Provider (line 909) | func (c *configOptions) OAuth2Provider() string {
method OAuth2RedirectURL (line 913) | func (c *configOptions) OAuth2RedirectURL() string {
method OAuth2UserCreation (line 917) | func (c *configOptions) OAuth2UserCreation() bool {
method PollingFrequency (line 921) | func (c *configOptions) PollingFrequency() time.Duration {
method PollingLimitPerHost (line 925) | func (c *configOptions) PollingLimitPerHost() int {
method PollingParsingErrorLimit (line 929) | func (c *configOptions) PollingParsingErrorLimit() int {
method PollingScheduler (line 933) | func (c *configOptions) PollingScheduler() string {
method Port (line 937) | func (c *configOptions) Port() string {
method RunMigrations (line 941) | func (c *configOptions) RunMigrations() bool {
method SetLogLevel (line 945) | func (c *configOptions) SetLogLevel(level string) {
method SetHTTPSValue (line 950) | func (c *configOptions) SetHTTPSValue(value bool) {
method SchedulerEntryFrequencyFactor (line 959) | func (c *configOptions) SchedulerEntryFrequencyFactor() int {
method SchedulerEntryFrequencyMaxInterval (line 963) | func (c *configOptions) SchedulerEntryFrequencyMaxInterval() time.Dura...
method SchedulerEntryFrequencyMinInterval (line 967) | func (c *configOptions) SchedulerEntryFrequencyMinInterval() time.Dura...
method SchedulerRoundRobinMaxInterval (line 971) | func (c *configOptions) SchedulerRoundRobinMaxInterval() time.Duration {
method SchedulerRoundRobinMinInterval (line 975) | func (c *configOptions) SchedulerRoundRobinMinInterval() time.Duration {
method TrustedReverseProxyNetworks (line 979) | func (c *configOptions) TrustedReverseProxyNetworks() []string {
method Watchdog (line 983) | func (c *configOptions) Watchdog() bool {
method WebAuthn (line 987) | func (c *configOptions) WebAuthn() bool {
method WorkerPoolSize (line 991) | func (c *configOptions) WorkerPoolSize() int {
method YouTubeAPIKey (line 995) | func (c *configOptions) YouTubeAPIKey() string {
method YouTubeEmbedUrlOverride (line 999) | func (c *configOptions) YouTubeEmbedUrlOverride() string {
method YouTubeEmbedDomain (line 1003) | func (c *configOptions) YouTubeEmbedDomain() string {
method ConfigMap (line 1007) | func (c *configOptions) ConfigMap(redactSecret bool) []*optionPair {
method String (line 1021) | func (c *configOptions) String() string {
function NewConfigOptions (line 63) | func NewConfigOptions() *configOptions {
FILE: internal/config/options_parsing_test.go
function TestBaseURLOptionParsing (line 11) | func TestBaseURLOptionParsing(t *testing.T) {
function TestWatchdogOptionParsing (line 63) | func TestWatchdogOptionParsing(t *testing.T) {
function TestWebAuthnOptionParsing (line 99) | func TestWebAuthnOptionParsing(t *testing.T) {
function TestWorkerPoolSizeOptionParsing (line 115) | func TestWorkerPoolSizeOptionParsing(t *testing.T) {
function TestYouTubeAPIKeyOptionParsing (line 131) | func TestYouTubeAPIKeyOptionParsing(t *testing.T) {
function TestAdminPasswordOptionParsing (line 147) | func TestAdminPasswordOptionParsing(t *testing.T) {
function TestAdminUsernameOptionParsing (line 163) | func TestAdminUsernameOptionParsing(t *testing.T) {
function TestAuthProxyHeaderOptionParsing (line 179) | func TestAuthProxyHeaderOptionParsing(t *testing.T) {
function TestAuthProxyUserCreationOptionParsing (line 195) | func TestAuthProxyUserCreationOptionParsing(t *testing.T) {
function TestBatchSizeOptionParsing (line 231) | func TestBatchSizeOptionParsing(t *testing.T) {
function TestCertDomainOptionParsing (line 247) | func TestCertDomainOptionParsing(t *testing.T) {
function TestCertFileOptionParsing (line 263) | func TestCertFileOptionParsing(t *testing.T) {
function TestCleanupArchiveBatchSizeOptionParsing (line 279) | func TestCleanupArchiveBatchSizeOptionParsing(t *testing.T) {
function TestCreateAdminOptionParsing (line 295) | func TestCreateAdminOptionParsing(t *testing.T) {
function TestDatabaseMaxConnsOptionParsing (line 319) | func TestDatabaseMaxConnsOptionParsing(t *testing.T) {
function TestDatabaseMinConnsOptionParsing (line 335) | func TestDatabaseMinConnsOptionParsing(t *testing.T) {
function TestDatabaseURLOptionParsing (line 351) | func TestDatabaseURLOptionParsing(t *testing.T) {
function TestDisableHSTSOptionParsing (line 375) | func TestDisableHSTSOptionParsing(t *testing.T) {
function TestDisableHTTPServiceOptionParsing (line 411) | func TestDisableHTTPServiceOptionParsing(t *testing.T) {
function TestDisableLocalAuthOptionParsing (line 447) | func TestDisableLocalAuthOptionParsing(t *testing.T) {
function TestDisableSchedulerServiceOptionParsing (line 471) | func TestDisableSchedulerServiceOptionParsing(t *testing.T) {
function TestFetchBilibiliWatchTimeOptionParsing (line 507) | func TestFetchBilibiliWatchTimeOptionParsing(t *testing.T) {
function TestFetchNebulaWatchTimeOptionParsing (line 531) | func TestFetchNebulaWatchTimeOptionParsing(t *testing.T) {
function TestFetchOdyseeWatchTimeOptionParsing (line 555) | func TestFetchOdyseeWatchTimeOptionParsing(t *testing.T) {
function TestFetchYouTubeWatchTimeOptionParsing (line 579) | func TestFetchYouTubeWatchTimeOptionParsing(t *testing.T) {
function TestHTTPClientMaxBodySizeOptionParsing (line 603) | func TestHTTPClientMaxBodySizeOptionParsing(t *testing.T) {
function TestHTTPClientUserAgentOptionParsing (line 621) | func TestHTTPClientUserAgentOptionParsing(t *testing.T) {
function TestHTTPSOptionParsing (line 637) | func TestHTTPSOptionParsing(t *testing.T) {
function TestInvidiousInstanceOptionParsing (line 661) | func TestInvidiousInstanceOptionParsing(t *testing.T) {
function TestCertKeyFileOptionParsing (line 677) | func TestCertKeyFileOptionParsing(t *testing.T) {
function TestLogDateTimeOptionParsing (line 693) | func TestLogDateTimeOptionParsing(t *testing.T) {
function TestLogFileOptionParsing (line 717) | func TestLogFileOptionParsing(t *testing.T) {
function TestLogFormatOptionParsing (line 733) | func TestLogFormatOptionParsing(t *testing.T) {
function TestLogLevelOptionParsing (line 749) | func TestLogLevelOptionParsing(t *testing.T) {
function TestMaintenanceMessageOptionParsing (line 765) | func TestMaintenanceMessageOptionParsing(t *testing.T) {
function TestMaintenanceModeOptionParsing (line 781) | func TestMaintenanceModeOptionParsing(t *testing.T) {
function TestMediaProxyModeOptionParsing (line 817) | func TestMediaProxyModeOptionParsing(t *testing.T) {
function TestMetricsCollectorOptionParsing (line 833) | func TestMetricsCollectorOptionParsing(t *testing.T) {
function TestMetricsPasswordOptionParsing (line 869) | func TestMetricsPasswordOptionParsing(t *testing.T) {
function TestMetricsUsernameOptionParsing (line 885) | func TestMetricsUsernameOptionParsing(t *testing.T) {
function TestOAuth2ClientIDOptionParsing (line 901) | func TestOAuth2ClientIDOptionParsing(t *testing.T) {
function TestOAuth2ClientSecretOptionParsing (line 917) | func TestOAuth2ClientSecretOptionParsing(t *testing.T) {
function TestOAuth2OIDCDiscoveryEndpointOptionParsing (line 933) | func TestOAuth2OIDCDiscoveryEndpointOptionParsing(t *testing.T) {
function TestOAuth2OIDCProviderNameOptionParsing (line 949) | func TestOAuth2OIDCProviderNameOptionParsing(t *testing.T) {
function TestOAuth2ProviderOptionParsing (line 965) | func TestOAuth2ProviderOptionParsing(t *testing.T) {
function TestOAuth2RedirectURLOptionParsing (line 993) | func TestOAuth2RedirectURLOptionParsing(t *testing.T) {
function TestOAuth2UserCreationOptionParsing (line 1009) | func TestOAuth2UserCreationOptionParsing(t *testing.T) {
function TestPollingLimitPerHostOptionParsing (line 1045) | func TestPollingLimitPerHostOptionParsing(t *testing.T) {
function TestPollingParsingErrorLimitOptionParsing (line 1061) | func TestPollingParsingErrorLimitOptionParsing(t *testing.T) {
function TestPollingSchedulerOptionParsing (line 1077) | func TestPollingSchedulerOptionParsing(t *testing.T) {
function TestPortOptionParsing (line 1093) | func TestPortOptionParsing(t *testing.T) {
function TestRunMigrationsOptionParsing (line 1114) | func TestRunMigrationsOptionParsing(t *testing.T) {
function TestSchedulerEntryFrequencyFactorOptionParsing (line 1138) | func TestSchedulerEntryFrequencyFactorOptionParsing(t *testing.T) {
function TestYouTubeEmbedUrlOverrideOptionParsing (line 1154) | func TestYouTubeEmbedUrlOverrideOptionParsing(t *testing.T) {
function TestCleanupArchiveReadIntervalOptionParsing (line 1196) | func TestCleanupArchiveReadIntervalOptionParsing(t *testing.T) {
function TestCleanupArchiveUnreadIntervalOptionParsing (line 1212) | func TestCleanupArchiveUnreadIntervalOptionParsing(t *testing.T) {
function TestCleanupFrequencyOptionParsing (line 1228) | func TestCleanupFrequencyOptionParsing(t *testing.T) {
function TestCleanupRemoveSessionsIntervalOptionParsing (line 1244) | func TestCleanupRemoveSessionsIntervalOptionParsing(t *testing.T) {
function TestDatabaseConnectionLifetimeOptionParsing (line 1260) | func TestDatabaseConnectionLifetimeOptionParsing(t *testing.T) {
function TestForceRefreshIntervalOptionParsing (line 1276) | func TestForceRefreshIntervalOptionParsing(t *testing.T) {
function TestHTTPClientProxiesOptionParsing (line 1292) | func TestHTTPClientProxiesOptionParsing(t *testing.T) {
function TestHTTPClientProxyOptionParsing (line 1313) | func TestHTTPClientProxyOptionParsing(t *testing.T) {
function TestHTTPClientTimeoutOptionParsing (line 1338) | func TestHTTPClientTimeoutOptionParsing(t *testing.T) {
function TestFetcherAllowPrivateNetworksOptionParsing (line 1354) | func TestFetcherAllowPrivateNetworksOptionParsing(t *testing.T) {
function TestIntegrationAllowPrivateNetworksOptionParsing (line 1378) | func TestIntegrationAllowPrivateNetworksOptionParsing(t *testing.T) {
function TestHTTPServerTimeoutOptionParsing (line 1402) | func TestHTTPServerTimeoutOptionParsing(t *testing.T) {
function TestListenAddrOptionParsing (line 1418) | func TestListenAddrOptionParsing(t *testing.T) {
function TestMediaCustomProxyURLOptionParsing (line 1436) | func TestMediaCustomProxyURLOptionParsing(t *testing.T) {
function TestMediaProxyHTTPClientTimeoutOptionParsing (line 1453) | func TestMediaProxyHTTPClientTimeoutOptionParsing(t *testing.T) {
function TestMediaProxyPrivateKeyOptionParsing (line 1469) | func TestMediaProxyPrivateKeyOptionParsing(t *testing.T) {
function TestMediaProxyResourceTypesOptionParsing (line 1486) | func TestMediaProxyResourceTypesOptionParsing(t *testing.T) {
function TestMetricsAllowedNetworksOptionParsing (line 1508) | func TestMetricsAllowedNetworksOptionParsing(t *testing.T) {
function TestMetricsRefreshIntervalOptionParsing (line 1526) | func TestMetricsRefreshIntervalOptionParsing(t *testing.T) {
function TestPollingFrequencyOptionParsing (line 1542) | func TestPollingFrequencyOptionParsing(t *testing.T) {
function TestSchedulerEntryFrequencyMaxIntervalOptionParsing (line 1558) | func TestSchedulerEntryFrequencyMaxIntervalOptionParsing(t *testing.T) {
function TestSchedulerEntryFrequencyMinIntervalOptionParsing (line 1574) | func TestSchedulerEntryFrequencyMinIntervalOptionParsing(t *testing.T) {
function TestSchedulerRoundRobinMaxIntervalOptionParsing (line 1590) | func TestSchedulerRoundRobinMaxIntervalOptionParsing(t *testing.T) {
function TestSchedulerRoundRobinMinIntervalOptionParsing (line 1606) | func TestSchedulerRoundRobinMinIntervalOptionParsing(t *testing.T) {
function TestTrustedReverseProxyNetworksOptionParsing (line 1622) | func TestTrustedReverseProxyNetworksOptionParsing(t *testing.T) {
function TestYouTubeEmbedDomainOptionParsing (line 1653) | func TestYouTubeEmbedDomainOptionParsing(t *testing.T) {
function TestSetLogLevelFunction (line 1670) | func TestSetLogLevelFunction(t *testing.T) {
function TestSetHTTPSValueFunction (line 1697) | func TestSetHTTPSValueFunction(t *testing.T) {
function TestConfigMap (line 1719) | func TestConfigMap(t *testing.T) {
function TestConfigMapWithRedactedSecrets (line 1732) | func TestConfigMapWithRedactedSecrets(t *testing.T) {
FILE: internal/config/parser.go
type configParser (line 21) | type configParser struct
method ParseEnvironmentVariables (line 31) | func (cp *configParser) ParseEnvironmentVariables() (*configOptions, e...
method ParseFile (line 39) | func (cp *configParser) ParseFile(filename string) (*configOptions, er...
method postParsing (line 53) | func (cp *configParser) postParsing() error {
method parseLines (line 100) | func (cp *configParser) parseLines(lines []string) error {
method parseLine (line 120) | func (cp *configParser) parseLine(key, value string) error {
function NewConfigParser (line 25) | func NewConfigParser() *configParser {
function parseStringValue (line 199) | func parseStringValue(value string, fallback string) string {
function parseBoolValue (line 206) | func parseBoolValue(value string, fallback bool) (bool, error) {
function parseIntValue (line 222) | func parseIntValue(value string, fallback int) int {
function ParsedInt64Value (line 235) | func ParsedInt64Value(value string, fallback int64) int64 {
function parseStringListValue (line 248) | func parseStringListValue(value string, fallback []string) []string {
function parseDurationValue (line 268) | func parseDurationValue(value string, unit time.Duration, fallback time....
function parseURLValue (line 281) | func parseURLValue(value string, fallback *url.URL) (*url.URL, error) {
function readSecretFileValue (line 294) | func readSecretFileValue(filename string) (string, error) {
function parseFileContent (line 308) | func parseFileContent(r io.Reader) (lines []string) {
FILE: internal/config/parser_test.go
function TestParseStringValue (line 14) | func TestParseStringValue(t *testing.T) {
function TestParseBoolValue (line 34) | func TestParseBoolValue(t *testing.T) {
function TestParseIntValue (line 75) | func TestParseIntValue(t *testing.T) {
function TestParsedInt64Value (line 101) | func TestParsedInt64Value(t *testing.T) {
function TestParseStringListValue (line 121) | func TestParseStringListValue(t *testing.T) {
function TestParseDurationValue (line 158) | func TestParseDurationValue(t *testing.T) {
function TestParseURLValue (line 187) | func TestParseURLValue(t *testing.T) {
function TestConfigFileParsing (line 217) | func TestConfigFileParsing(t *testing.T) {
function TestConfigFileParsingWithIncorrectKeyValuePair (line 267) | func TestConfigFileParsingWithIncorrectKeyValuePair(t *testing.T) {
function TestParseAdminPasswordFileOption (line 294) | func TestParseAdminPasswordFileOption(t *testing.T) {
function TestParseAdminPasswordFileOptionWithEmptyFile (line 321) | func TestParseAdminPasswordFileOptionWithEmptyFile(t *testing.T) {
function TestParseLogFileOptionDefaultValue (line 339) | func TestParseLogFileOptionDefaultValue(t *testing.T) {
function TestParseLogFileOptionWithCustomFilename (line 353) | func TestParseLogFileOptionWithCustomFilename(t *testing.T) {
function TestParseLogFileOptionWithEmptyValue (line 368) | func TestParseLogFileOptionWithEmptyValue(t *testing.T) {
function TestParseLogDateTimeOptionDefaultValue (line 383) | func TestParseLogDateTimeOptionDefaultValue(t *testing.T) {
function TestParseLogDateTimeOptionWithCustomValue (line 397) | func TestParseLogDateTimeOptionWithCustomValue(t *testing.T) {
function TestParseLogDateTimeOptionWithEmptyValue (line 412) | func TestParseLogDateTimeOptionWithEmptyValue(t *testing.T) {
function TestParseLogDateTimeOptionWithIncorrectValue (line 427) | func TestParseLogDateTimeOptionWithIncorrectValue(t *testing.T) {
FILE: internal/config/validators.go
function validateChoices (line 14) | func validateChoices(rawValue string, choices []string) error {
function validateListChoices (line 21) | func validateListChoices(inputValues, choices []string) error {
function validateGreaterThan (line 30) | func validateGreaterThan(rawValue string, min int) error {
function validateGreaterOrEqualThan (line 41) | func validateGreaterOrEqualThan(rawValue string, min int) error {
function validateRange (line 52) | func validateRange(rawValue string, min, max int) error {
FILE: internal/config/validators_test.go
function TestValidateChoices (line 11) | func TestValidateChoices(t *testing.T) {
function TestValidateListChoices (line 90) | func TestValidateListChoices(t *testing.T) {
function TestValidateGreaterThan (line 187) | func TestValidateGreaterThan(t *testing.T) {
function TestValidateGreaterOrEqualThan (line 205) | func TestValidateGreaterOrEqualThan(t *testing.T) {
function TestValidateRange (line 223) | func TestValidateRange(t *testing.T) {
FILE: internal/crypto/crypto.go
function HashFromBytes (line 19) | func HashFromBytes(value []byte) string {
function SHA256 (line 26) | func SHA256(value string) string {
function GenerateRandomBytes (line 32) | func GenerateRandomBytes(size int) []byte {
function GenerateRandomStringHex (line 39) | func GenerateRandomStringHex(size int) string {
function HashPassword (line 43) | func HashPassword(password string) (string, error) {
function GenerateSHA256Hmac (line 48) | func GenerateSHA256Hmac(secret string, data []byte) string {
function GenerateUUID (line 54) | func GenerateUUID() string {
function ConstantTimeCmp (line 59) | func ConstantTimeCmp(a, b string) bool {
FILE: internal/database/database.go
function Migrate (line 13) | func Migrate(db *sql.DB) error {
function IsSchemaUpToDate (line 54) | func IsSchemaUpToDate(db *sql.DB) error {
FILE: internal/database/postgresql.go
function NewConnectionPool (line 14) | func NewConnectionPool(dsn string, minConnections, maxConnections int, c...
FILE: internal/fever/handler.go
function NewHandler (line 22) | func NewHandler(store *storage.Storage) http.Handler {
type feverHandler (line 27) | type feverHandler struct
method serve (line 31) | func (h *feverHandler) serve(w http.ResponseWriter, r *http.Request) {
method handleGroups (line 75) | func (h *feverHandler) handleGroups(w http.ResponseWriter, r *http.Req...
method handleFeeds (line 127) | func (h *feverHandler) handleFeeds(w http.ResponseWriter, r *http.Requ...
method handleFavicons (line 182) | func (h *feverHandler) handleFavicons(w http.ResponseWriter, r *http.R...
method handleItems (line 236) | func (h *feverHandler) handleItems(w http.ResponseWriter, r *http.Requ...
method handleUnreadItems (line 341) | func (h *feverHandler) handleUnreadItems(w http.ResponseWriter, r *htt...
method handleSavedItems (line 374) | func (h *feverHandler) handleSavedItems(w http.ResponseWriter, r *http...
method handleWriteItems (line 404) | func (h *feverHandler) handleWriteItems(w http.ResponseWriter, r *http...
method handleWriteFeeds (line 486) | func (h *feverHandler) handleWriteFeeds(w http.ResponseWriter, r *http...
method handleWriteGroups (line 515) | func (h *feverHandler) handleWriteGroups(w http.ResponseWriter, r *htt...
function buildFeedGroups (line 554) | func buildFeedGroups(feeds model.Feeds) []feedsGroups {
FILE: internal/fever/middleware.go
function Middleware (line 17) | func Middleware(store *storage.Storage) func(http.Handler) http.Handler {
FILE: internal/fever/response.go
type baseResponse (line 10) | type baseResponse struct
method SetCommonValues (line 16) | func (b *baseResponse) SetCommonValues() {
function newBaseResponse (line 40) | func newBaseResponse() baseResponse {
function newAuthFailureResponse (line 46) | func newAuthFailureResponse() baseResponse {
type groupsResponse (line 50) | type groupsResponse struct
type feedsResponse (line 56) | type feedsResponse struct
type faviconsResponse (line 62) | type faviconsResponse struct
type itemsResponse (line 67) | type itemsResponse struct
type unreadResponse (line 73) | type unreadResponse struct
type savedResponse (line 78) | type savedResponse struct
type group (line 83) | type group struct
type feedsGroups (line 88) | type feedsGroups struct
type feed (line 93) | type feed struct
type item (line 103) | type item struct
type favicon (line 115) | type favicon struct
FILE: internal/googlereader/handler.go
function NewHandler (line 38) | func NewHandler(store *storage.Storage) http.Handler {
type greaderHandler (line 68) | type greaderHandler struct
method clientLoginHandler (line 72) | func (h *greaderHandler) clientLoginHandler(w http.ResponseWriter, r *...
method tokenHandler (line 149) | func (h *greaderHandler) tokenHandler(w http.ResponseWriter, r *http.R...
method editTagHandler (line 187) | func (h *greaderHandler) editTagHandler(w http.ResponseWriter, r *http...
method quickAddHandler (line 324) | func (h *greaderHandler) quickAddHandler(w http.ResponseWriter, r *htt...
method feedIconURL (line 519) | func (h *greaderHandler) feedIconURL(f *model.Feed) string {
method editSubscriptionHandler (line 526) | func (h *greaderHandler) editSubscriptionHandler(w http.ResponseWriter...
method streamItemContentsHandler (line 605) | func (h *greaderHandler) streamItemContentsHandler(w http.ResponseWrit...
method disableTagHandler (line 739) | func (h *greaderHandler) disableTagHandler(w http.ResponseWriter, r *h...
method renameTagHandler (line 780) | func (h *greaderHandler) renameTagHandler(w http.ResponseWriter, r *ht...
method tagListHandler (line 847) | func (h *greaderHandler) tagListHandler(w http.ResponseWriter, r *http...
method subscriptionListHandler (line 883) | func (h *greaderHandler) subscriptionListHandler(w http.ResponseWriter...
method fallbackHandler (line 920) | func (h *greaderHandler) fallbackHandler(w http.ResponseWriter, r *htt...
method userInfoHandler (line 932) | func (h *greaderHandler) userInfoHandler(w http.ResponseWriter, r *htt...
method streamItemIDsHandler (line 955) | func (h *greaderHandler) streamItemIDsHandler(w http.ResponseWriter, r...
method handleReadingListStreamHandler (line 1008) | func (h *greaderHandler) handleReadingListStreamHandler(w http.Respons...
method handleStarredStreamHandler (line 1051) | func (h *greaderHandler) handleStarredStreamHandler(w http.ResponseWri...
method handleReadStreamHandler (line 1072) | func (h *greaderHandler) handleReadStreamHandler(w http.ResponseWriter...
method handleFeedStreamHandler (line 1116) | func (h *greaderHandler) handleFeedStreamHandler(w http.ResponseWriter...
method markAllAsReadHandler (line 1153) | func (h *greaderHandler) markAllAsReadHandler(w http.ResponseWriter, r...
function getFeed (line 395) | func getFeed(stream Stream, store *storage.Storage, userID int64) (*mode...
function getOrCreateCategory (line 403) | func getOrCreateCategory(streamCategory Stream, store *storage.Storage, ...
function subscribe (line 416) | func subscribe(newFeed Stream, category Stream, title string, store *sto...
function unsubscribe (line 449) | func unsubscribe(streams []Stream, store *storage.Storage, userID int64)...
function rename (line 463) | func rename(feedStream Stream, title string, store *storage.Storage, use...
function move (line 489) | func move(feedStream Stream, labelStream Stream, store *storage.Storage,...
function getItemRefsAndContinuation (line 1094) | func getItemRefsAndContinuation(builder storage.EntryQueryBuilder, rm re...
function checkAndSimplifyTags (line 1232) | func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[St...
function checkOutputFormat (line 1281) | func checkOutputFormat(r *http.Request) error {
FILE: internal/googlereader/item.go
constant ItemIDPrefix (line 15) | ItemIDPrefix = "tag:google.com,2005:reader/item/"
constant ItemIDFormat (line 16) | ItemIDFormat = "tag:google.com,2005:reader/item/%016x"
function convertEntryIDToLongFormItemID (line 19) | func convertEntryIDToLongFormItemID(entryID int64) string {
function parseItemID (line 29) | func parseItemID(itemIDValue string) (int64, error) {
function parseItemIDsFromRequest (line 59) | func parseItemIDsFromRequest(r *http.Request) ([]int64, error) {
FILE: internal/googlereader/item_test.go
function TestConvertEntryIDToLongFormItemID (line 13) | func TestConvertEntryIDToLongFormItemID(t *testing.T) {
function TestParseItemIDsFromRequest (line 23) | func TestParseItemIDsFromRequest(t *testing.T) {
function TestParseItemID (line 56) | func TestParseItemID(t *testing.T) {
FILE: internal/googlereader/middleware.go
type authMiddleware (line 20) | type authMiddleware struct
method validateApiKey (line 28) | func (m *authMiddleware) validateApiKey(next http.Handler) http.Handler {
function newAuthMiddleware (line 24) | func newAuthMiddleware(s *storage.Storage) *authMiddleware {
function getAuthToken (line 177) | func getAuthToken(username, password string) string {
FILE: internal/googlereader/parameters.go
constant paramItemIDs (line 8) | paramItemIDs = "i"
constant paramStreamID (line 10) | paramStreamID = "s"
constant paramStreamExcludes (line 12) | paramStreamExcludes = "xt"
constant paramStreamFilters (line 14) | paramStreamFilters = "it"
constant paramStreamMaxItems (line 16) | paramStreamMaxItems = "n"
constant paramStreamOrder (line 18) | paramStreamOrder = "r"
constant paramStreamStartTime (line 20) | paramStreamStartTime = "ot"
constant paramStreamStopTime (line 22) | paramStreamStopTime = "nt"
constant paramTagsRemove (line 24) | paramTagsRemove = "r"
constant paramTagsAdd (line 26) | paramTagsAdd = "a"
constant paramSubscribeAction (line 28) | paramSubscribeAction = "ac"
constant paramTitle (line 30) | paramTitle = "t"
constant paramQuickAdd (line 32) | paramQuickAdd = "quickadd"
constant paramDestination (line 34) | paramDestination = "dest"
constant paramContinuation (line 36) | paramContinuation = "c"
constant paramTimestamp (line 38) | paramTimestamp = "ts"
FILE: internal/googlereader/prefix_suffix.go
constant streamPrefix (line 8) | streamPrefix = "user/-/state/com.google/"
constant userStreamPrefix (line 10) | userStreamPrefix = "user/%d/state/com.google/"
constant labelPrefix (line 12) | labelPrefix = "user/-/label/"
constant userLabelPrefix (line 14) | userLabelPrefix = "user/%d/label/"
constant feedPrefix (line 16) | feedPrefix = "feed/"
constant readStreamSuffix (line 18) | readStreamSuffix = "read"
constant starredStreamSuffix (line 20) | starredStreamSuffix = "starred"
constant readingListStreamSuffix (line 22) | readingListStreamSuffix = "reading-list"
constant keptUnreadStreamSuffix (line 24) | keptUnreadStreamSuffix = "kept-unread"
constant broadcastStreamSuffix (line 26) | broadcastStreamSuffix = "broadcast"
constant broadcastFriendsStreamSuffix (line 28) | broadcastFriendsStreamSuffix = "broadcast-friends"
constant likeStreamSuffix (line 30) | likeStreamSuffix = "like"
FILE: internal/googlereader/request_modifier.go
type requestModifiers (line 14) | type requestModifiers struct
method String (line 27) | func (r requestModifiers) String() string {
function parseStreamFilterFromRequest (line 60) | func parseStreamFilterFromRequest(r *http.Request) (requestModifiers, er...
FILE: internal/googlereader/response.go
type loginResponse (line 13) | type loginResponse struct
method String (line 19) | func (l loginResponse) String() string {
type userInfoResponse (line 23) | type userInfoResponse struct
type subscriptionResponse (line 30) | type subscriptionResponse struct
type subscriptionsResponse (line 39) | type subscriptionsResponse struct
type quickAddResponse (line 43) | type quickAddResponse struct
type subscriptionCategoryResponse (line 50) | type subscriptionCategoryResponse struct
type itemRef (line 56) | type itemRef struct
type streamIDResponse (line 62) | type streamIDResponse struct
type tagsResponse (line 67) | type tagsResponse struct
type streamContentItemsResponse (line 71) | type streamContentItemsResponse struct
type contentItem (line 81) | type contentItem struct
type contentHREFType (line 98) | type contentHREFType struct
type contentHREF (line 103) | type contentHREF struct
type contentItemEnclosure (line 107) | type contentItemEnclosure struct
type contentItemContent (line 111) | type contentItemContent struct
type contentItemOrigin (line 116) | type contentItemOrigin struct
function sendUnauthorizedResponse (line 122) | func sendUnauthorizedResponse(w http.ResponseWriter, r *http.Request) {
FILE: internal/googlereader/stream.go
type StreamType (line 11) | type StreamType
method String (line 46) | func (st StreamType) String() string {
constant NoStream (line 15) | NoStream StreamType = iota
constant ReadStream (line 17) | ReadStream
constant StarredStream (line 19) | StarredStream
constant ReadingListStream (line 21) | ReadingListStream
constant KeptUnreadStream (line 23) | KeptUnreadStream
constant BroadcastStream (line 25) | BroadcastStream
constant BroadcastFriendsStream (line 27) | BroadcastFriendsStream
constant LabelStream (line 29) | LabelStream
constant FeedStream (line 31) | FeedStream
constant LikeStream (line 33) | LikeStream
type Stream (line 37) | type Stream struct
method String (line 42) | func (s Stream) String() string {
function getStream (line 73) | func getStream(streamID string, userID int64) (Stream, error) {
function getStreams (line 109) | func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
FILE: internal/http/client/client.go
type Options (line 21) | type Options struct
function NewClientWithOptions (line 27) | func NewClientWithOptions(opts Options) *http.Client {
FILE: internal/http/client/client_test.go
function TestNewClientWithoutBlockingPrivateNetworks (line 15) | func TestNewClientWithoutBlockingPrivateNetworks(t *testing.T) {
function TestBlockPrivateNetworksBlocksLoopback (line 33) | func TestBlockPrivateNetworksBlocksLoopback(t *testing.T) {
function TestBlockPrivateNetworksAllowsPublicIPs (line 50) | func TestBlockPrivateNetworksAllowsPublicIPs(t *testing.T) {
function TestNoCustomTransportWhenNotBlocking (line 65) | func TestNoCustomTransportWhenNotBlocking(t *testing.T) {
function TestBlockPrivateNetworksBlocksPrivateIP (line 72) | func TestBlockPrivateNetworksBlocksPrivateIP(t *testing.T) {
function TestBlockPrivateNetworksAllowsLoopbackWhenDisabled (line 97) | func TestBlockPrivateNetworksAllowsLoopbackWhenDisabled(t *testing.T) {
FILE: internal/http/cookie/cookie.go
constant CookieAppSessionID (line 15) | CookieAppSessionID = "MinifluxAppSessionID"
constant CookieUserSessionID (line 16) | CookieUserSessionID = "MinifluxUserSessionID"
function New (line 20) | func New(name, value string, isHTTPS bool, path string) *http.Cookie {
function Expired (line 33) | func Expired(name string, isHTTPS bool, path string) *http.Cookie {
function basePath (line 46) | func basePath(path string) string {
FILE: internal/http/request/client_ip.go
function IsTrustedIP (line 13) | func IsTrustedIP(remoteIP string, trustedNetworks []string) bool {
function FindClientIP (line 38) | func FindClientIP(r *http.Request, isTrustedProxyClient bool) string {
function FindRemoteIP (line 62) | func FindRemoteIP(r *http.Request) string {
function dropIPv6zone (line 85) | func dropIPv6zone(address string) string {
FILE: internal/http/request/client_ip_test.go
function TestFindClientIPWithoutHeaders (line 11) | func TestFindClientIPWithoutHeaders(t *testing.T) {
function TestFindClientIPWithXFFHeader (line 48) | func TestFindClientIPWithXFFHeader(t *testing.T) {
function TestClientIPWithXRealIPHeader (line 95) | func TestClientIPWithXRealIPHeader(t *testing.T) {
function TestClientIPWithBothHeaders (line 105) | func TestClientIPWithBothHeaders(t *testing.T) {
function TestClientIPWithUnixSocketRemoteAddrAndBothHeaders (line 117) | func TestClientIPWithUnixSocketRemoteAddrAndBothHeaders(t *testing.T) {
function TestIsTrustedIP (line 129) | func TestIsTrustedIP(t *testing.T) {
function TestFindRemoteIP (line 162) | func TestFindRemoteIP(t *testing.T) {
FILE: internal/http/request/context.go
type ContextKey (line 15) | type ContextKey
constant UserIDContextKey (line 19) | UserIDContextKey ContextKey = iota
constant UserNameContextKey (line 20) | UserNameContextKey
constant UserTimezoneContextKey (line 21) | UserTimezoneContextKey
constant IsAdminUserContextKey (line 22) | IsAdminUserContextKey
constant IsAuthenticatedContextKey (line 23) | IsAuthenticatedContextKey
constant UserSessionTokenContextKey (line 24) | UserSessionTokenContextKey
constant UserLanguageContextKey (line 25) | UserLanguageContextKey
constant UserThemeContextKey (line 26) | UserThemeContextKey
constant SessionIDContextKey (line 27) | SessionIDContextKey
constant CSRFContextKey (line 28) | CSRFContextKey
constant OAuth2StateContextKey (line 29) | OAuth2StateContextKey
constant OAuth2CodeVerifierContextKey (line 30) | OAuth2CodeVerifierContextKey
constant FlashMessageContextKey (line 31) | FlashMessageContextKey
constant FlashErrorMessageContextKey (line 32) | FlashErrorMessageContextKey
constant LastForceRefreshContextKey (line 33) | LastForceRefreshContextKey
constant ClientIPContextKey (line 34) | ClientIPContextKey
constant GoogleReaderTokenKey (line 35) | GoogleReaderTokenKey
constant WebAuthnDataContextKey (line 36) | WebAuthnDataContextKey
function WebAuthnSessionData (line 40) | func WebAuthnSessionData(r *http.Request) *model.WebAuthnSession {
function GoogleReaderToken (line 50) | func GoogleReaderToken(r *http.Request) string {
function IsAdminUser (line 55) | func IsAdminUser(r *http.Request) bool {
function IsAuthenticated (line 60) | func IsAuthenticated(r *http.Request) bool {
function UserID (line 65) | func UserID(r *http.Request) int64 {
function UserName (line 70) | func UserName(r *http.Request) string {
function UserTimezone (line 79) | func UserTimezone(r *http.Request) string {
function UserLanguage (line 88) | func UserLanguage(r *http.Request) string {
function UserTheme (line 97) | func UserTheme(r *http.Request) string {
function CSRF (line 106) | func CSRF(r *http.Request) string {
function SessionID (line 111) | func SessionID(r *http.Request) string {
function UserSessionToken (line 116) | func UserSessionToken(r *http.Request) string {
function OAuth2State (line 121) | func OAuth2State(r *http.Request) string {
function OAuth2CodeVerifier (line 126) | func OAuth2CodeVerifier(r *http.Request) string {
function FlashMessage (line 131) | func FlashMessage(r *http.Request) string {
function FlashErrorMessage (line 136) | func FlashErrorMessage(r *http.Request) string {
function LastForceRefresh (line 141) | func LastForceRefresh(r *http.Request) time.Time {
function ClientIP (line 151) | func ClientIP(r *http.Request) string {
function getContextStringValue (line 155) | func getContextStringValue(r *http.Request, key ContextKey) string {
function getContextBoolValue (line 164) | func getContextBoolValue(r *http.Request, key ContextKey) bool {
function getContextInt64Value (line 173) | func getContextInt64Value(r *http.Request, key ContextKey) int64 {
FILE: internal/http/request/context_test.go
function TestContextStringValue (line 15) | func TestContextStringValue(t *testing.T) {
function TestContextStringValueWithInvalidType (line 29) | func TestContextStringValueWithInvalidType(t *testing.T) {
function TestContextStringValueWhenUnset (line 43) | func TestContextStringValueWhenUnset(t *testing.T) {
function TestContextBoolValue (line 54) | func TestContextBoolValue(t *testing.T) {
function TestContextBoolValueWithInvalidType (line 68) | func TestContextBoolValueWithInvalidType(t *testing.T) {
function TestContextBoolValueWhenUnset (line 82) | func TestContextBoolValueWhenUnset(t *testing.T) {
function TestContextInt64Value (line 93) | func TestContextInt64Value(t *testing.T) {
function TestContextInt64ValueWithInvalidType (line 107) | func TestContextInt64ValueWithInvalidType(t *testing.T) {
function TestContextInt64ValueWhenUnset (line 121) | func TestContextInt64ValueWhenUnset(t *testing.T) {
function TestIsAdmin (line 132) | func TestIsAdmin(t *testing.T) {
function TestIsAuthenticated (line 154) | func TestIsAuthenticated(t *testing.T) {
function TestUserID (line 176) | func TestUserID(t *testing.T) {
function TestUserName (line 198) | func TestUserName(t *testing.T) {
function TestUserTimezone (line 220) | func TestUserTimezone(t *testing.T) {
function TestUserLanguage (line 242) | func TestUserLanguage(t *testing.T) {
function TestUserTheme (line 264) | func TestUserTheme(t *testing.T) {
function TestCSRF (line 286) | func TestCSRF(t *testing.T) {
function TestSessionID (line 308) | func TestSessionID(t *testing.T) {
function TestUserSessionToken (line 330) | func TestUserSessionToken(t *testing.T) {
function TestOAuth2State (line 352) | func TestOAuth2State(t *testing.T) {
function TestOAuth2CodeVerifier (line 374) | func TestOAuth2CodeVerifier(t *testing.T) {
function TestFlashMessage (line 396) | func TestFlashMessage(t *testing.T) {
function TestFlashErrorMessage (line 418) | func TestFlashErrorMessage(t *testing.T) {
function TestLastForceRefresh (line 440) | func TestLastForceRefresh(t *testing.T) {
function TestWebAuthnSessionData (line 473) | func TestWebAuthnSessionData(t *testing.T) {
function TestClientIP (line 501) | func TestClientIP(t *testing.T) {
function TestGoogleReaderToken (line 523) | func TestGoogleReaderToken(t *testing.T) {
FILE: internal/http/request/cookie.go
function CookieValue (line 9) | func CookieValue(r *http.Request, name string) string {
FILE: internal/http/request/cookie_test.go
function TestGetCookieValue (line 11) | func TestGetCookieValue(t *testing.T) {
function TestGetCookieValueWhenUnset (line 23) | func TestGetCookieValueWhenUnset(t *testing.T) {
FILE: internal/http/request/params.go
function FormInt64Value (line 13) | func FormInt64Value(r *http.Request, param string) int64 {
function RouteInt64Param (line 24) | func RouteInt64Param(r *http.Request, param string) int64 {
function RouteStringParam (line 38) | func RouteStringParam(r *http.Request, param string) string {
function QueryStringParam (line 43) | func QueryStringParam(r *http.Request, param, defaultValue string) string {
function QueryStringParamList (line 52) | func QueryStringParamList(r *http.Request, param string) []string {
function QueryIntParam (line 69) | func QueryIntParam(r *http.Request, param string, defaultValue int) int {
function QueryInt64Param (line 88) | func QueryInt64Param(r *http.Request, param string, defaultValue int64) ...
function QueryBoolParam (line 107) | func QueryBoolParam(r *http.Request, param string, defaultValue bool) bo...
function HasQueryParam (line 123) | func HasQueryParam(r *http.Request, param string) bool {
function routeParam (line 129) | func routeParam(r *http.Request, param string) string {
FILE: internal/http/request/params_test.go
function TestFormInt64Value (line 14) | func TestFormInt64Value(t *testing.T) {
function TestRouteStringParamWithServerMux (line 43) | func TestRouteStringParamWithServerMux(t *testing.T) {
function TestRouteInt64ParamWithServerMux (line 70) | func TestRouteInt64ParamWithServerMux(t *testing.T) {
function TestQueryStringParam (line 111) | func TestQueryStringParam(t *testing.T) {
function TestQueryIntParam (line 130) | func TestQueryIntParam(t *testing.T) {
function TestQueryInt64Param (line 163) | func TestQueryInt64Param(t *testing.T) {
function TestQueryBoolParam (line 196) | func TestQueryBoolParam(t *testing.T) {
function TestQueryStringParamList (line 229) | func TestQueryStringParamList(t *testing.T) {
function TestHasQueryParam (line 248) | func TestHasQueryParam(t *testing.T) {
FILE: internal/http/response/builder.go
constant compressionThreshold (line 18) | compressionThreshold = 1024
type Builder (line 21) | type Builder struct
method WithStatus (line 36) | func (b *Builder) WithStatus(statusCode int) *Builder {
method WithHeader (line 42) | func (b *Builder) WithHeader(key, value string) *Builder {
method WithBodyAsBytes (line 48) | func (b *Builder) WithBodyAsBytes(body []byte) *Builder {
method WithBodyAsString (line 54) | func (b *Builder) WithBodyAsString(body string) *Builder {
method WithBodyAsReader (line 60) | func (b *Builder) WithBodyAsReader(body io.Reader) *Builder {
method WithAttachment (line 66) | func (b *Builder) WithAttachment(filename string) *Builder {
method WithoutCompression (line 72) | func (b *Builder) WithoutCompression() *Builder {
method WithCaching (line 78) | func (b *Builder) WithCaching(etag string, duration time.Duration, cal...
method Write (line 94) | func (b *Builder) Write() {
method writeHeaders (line 115) | func (b *Builder) writeHeaders() {
method compress (line 127) | func (b *Builder) compress(data []byte) {
function NewBuilder (line 31) | func NewBuilder(w http.ResponseWriter, r *http.Request) *Builder {
function normalizeETag (line 163) | func normalizeETag(etag string) string {
function ifNoneMatch (line 174) | func ifNoneMatch(headerValue, etag string) bool {
FILE: internal/http/response/builder_test.go
function TestResponseHasCommonHeaders (line 15) | func TestResponseHasCommonHeaders(t *testing.T) {
function TestBuildResponseWithCustomStatusCode (line 43) | func TestBuildResponseWithCustomStatusCode(t *testing.T) {
function TestBuildResponseWithCustomHeader (line 64) | func TestBuildResponseWithCustomHeader(t *testing.T) {
function TestBuildResponseWithAttachment (line 86) | func TestBuildResponseWithAttachment(t *testing.T) {
function TestBuildResponseWithByteBody (line 108) | func TestBuildResponseWithByteBody(t *testing.T) {
function TestBuildResponseWithCachingEnabled (line 129) | func TestBuildResponseWithCachingEnabled(t *testing.T) {
function TestBuildResponseWithCachingAndIfNoneMatch (line 173) | func TestBuildResponseWithCachingAndIfNoneMatch(t *testing.T) {
function TestNormalizeETag (line 226) | func TestNormalizeETag(t *testing.T) {
function TestIfNoneMatch (line 247) | func TestIfNoneMatch(t *testing.T) {
function TestBuildResponseWithBrotliCompression (line 273) | func TestBuildResponseWithBrotliCompression(t *testing.T) {
function TestBuildResponseWithGzipCompression (line 297) | func TestBuildResponseWithGzipCompression(t *testing.T) {
function TestBuildResponseWithDeflateCompression (line 321) | func TestBuildResponseWithDeflateCompression(t *testing.T) {
function TestBuildResponseWithCompressionDisabled (line 351) | func TestBuildResponseWithCompressionDisabled(t *testing.T) {
function TestBuildResponseWithDeflateCompressionAndSmallPayload (line 381) | func TestBuildResponseWithDeflateCompressionAndSmallPayload(t *testing.T) {
function TestBuildResponseWithoutCompressionHeader (line 411) | func TestBuildResponseWithoutCompressionHeader(t *testing.T) {
function TestBuildResponseWithReaderBody (line 440) | func TestBuildResponseWithReaderBody(t *testing.T) {
FILE: internal/http/response/html.go
function HTML (line 15) | func HTML[T []byte | string](w http.ResponseWriter, r *http.Request, bod...
function HTMLServerError (line 29) | func HTMLServerError(w http.ResponseWriter, r *http.Request, err error) {
function HTMLBadRequest (line 53) | func HTMLBadRequest(w http.ResponseWriter, r *http.Request, err error) {
function HTMLForbidden (line 77) | func HTMLForbidden(w http.ResponseWriter, r *http.Request) {
function HTMLNotFound (line 99) | func HTMLNotFound(w http.ResponseWriter, r *http.Request) {
function HTMLRedirect (line 121) | func HTMLRedirect(w http.ResponseWriter, r *http.Request, uri string) {
function HTMLRequestedRangeNotSatisfiable (line 126) | func HTMLRequestedRangeNotSatisfiable(w http.ResponseWriter, r *http.Req...
FILE: internal/http/response/html_test.go
function TestHTMLResponse (line 13) | func TestHTMLResponse(t *testing.T) {
function TestHTMLServerErrorResponse (line 48) | func TestHTMLServerErrorResponse(t *testing.T) {
function TestHTMLBadRequestResponse (line 76) | func TestHTMLBadRequestResponse(t *testing.T) {
function TestHTMLForbiddenResponse (line 104) | func TestHTMLForbiddenResponse(t *testing.T) {
function TestHTMLNotFoundResponse (line 132) | func TestHTMLNotFoundResponse(t *testing.T) {
function TestHTMLRedirectResponse (line 160) | func TestHTMLRedirectResponse(t *testing.T) {
function TestHTMLRequestedRangeNotSatisfiable (line 186) | func TestHTMLRequestedRangeNotSatisfiable(t *testing.T) {
FILE: internal/http/response/json.go
constant jsonContentTypeHeader (line 15) | jsonContentTypeHeader = `application/json`
function JSON (line 18) | func JSON(w http.ResponseWriter, r *http.Request, body any) {
function JSONCreated (line 32) | func JSONCreated(w http.ResponseWriter, r *http.Request, body any) {
function JSONAccepted (line 47) | func JSONAccepted(w http.ResponseWriter, r *http.Request) {
function JSONServerError (line 55) | func JSONServerError(w http.ResponseWriter, r *http.Request, err error) {
function JSONBadRequest (line 77) | func JSONBadRequest(w http.ResponseWriter, r *http.Request, err error) {
function JSONUnauthorized (line 99) | func JSONUnauthorized(w http.ResponseWriter, r *http.Request) {
function JSONForbidden (line 120) | func JSONForbidden(w http.ResponseWriter, r *http.Request) {
function JSONNotFound (line 141) | func JSONNotFound(w http.ResponseWriter, r *http.Request) {
function generateJSONError (line 161) | func generateJSONError(err error) []byte {
FILE: internal/http/response/json_test.go
function TestJSONResponse (line 13) | func TestJSONResponse(t *testing.T) {
function TestJSONCreatedResponse (line 43) | func TestJSONCreatedResponse(t *testing.T) {
function TestJSONAcceptedResponse (line 71) | func TestJSONAcceptedResponse(t *testing.T) {
function TestJSONServerErrorResponse (line 99) | func TestJSONServerErrorResponse(t *testing.T) {
function TestJSONBadRequestResponse (line 129) | func TestJSONBadRequestResponse(t *testing.T) {
function TestJSONUnauthorizedResponse (line 157) | func TestJSONUnauthorizedResponse(t *testing.T) {
function TestJSONForbiddenResponse (line 185) | func TestJSONForbiddenResponse(t *testing.T) {
function TestJSONNotFoundResponse (line 213) | func TestJSONNotFoundResponse(t *testing.T) {
function TestBuildInvalidJSONResponse (line 241) | func TestBuildInvalidJSONResponse(t *testing.T) {
function TestBuildInvalidJSONCreatedResponse (line 269) | func TestBuildInvalidJSONCreatedResponse(t *testing.T) {
function TestGenerateJSONError (line 297) | func TestGenerateJSONError(t *testing.T) {
FILE: internal/http/response/response.go
constant ContentSecurityPolicyForUntrustedContent (line 16) | ContentSecurityPolicyForUntrustedContent = `default-src 'none'; form-act...
function NoContent (line 19) | func NoContent(w http.ResponseWriter, r *http.Request) {
FILE: internal/http/response/response_test.go
function TestNoContentResponse (line 12) | func TestNoContentResponse(t *testing.T) {
FILE: internal/http/response/text.go
function Text (line 9) | func Text(w http.ResponseWriter, r *http.Request, body string) {
FILE: internal/http/response/text_test.go
function TestTextResponse (line 12) | func TestTextResponse(t *testing.T) {
FILE: internal/http/response/xml.go
function XML (line 9) | func XML(w http.ResponseWriter, r *http.Request, body string) {
function XMLAttachment (line 17) | func XMLAttachment(w http.ResponseWriter, r *http.Request, filename stri...
FILE: internal/http/response/xml_test.go
function TestXMLResponse (line 12) | func TestXMLResponse(t *testing.T) {
function TestXMLAttachmentResponse (line 40) | func TestXMLAttachmentResponse(t *testing.T) {
FILE: internal/http/server/healthcheck.go
function livenessProbe (line 13) | func livenessProbe(w http.ResponseWriter, r *http.Request) {
function newReadinessProbe (line 18) | func newReadinessProbe(store *storage.Storage) http.HandlerFunc {
FILE: internal/http/server/httpd.go
function StartWebServer (line 24) | func StartWebServer(store *storage.Storage, pool *worker.Pool) []*http.S...
function startSystemdSocketServer (line 103) | func startSystemdSocketServer(server *http.Server) {
function startUnixSocketServer (line 118) | func startUnixSocketServer(server *http.Server, socketFile string) {
function startAutoCertTLSServer (line 155) | func startAutoCertTLSServer(server *http.Server, autoTLSConfig *tls.Conf...
function startTLSServer (line 172) | func startTLSServer(server *http.Server, certFile, keyFile string) {
function startHTTPServer (line 185) | func startHTTPServer(server *http.Server) {
function printErrorAndExit (line 196) | func printErrorAndExit(format string, a ...any) {
FILE: internal/http/server/metrics.go
function metricsHandler (line 16) | func metricsHandler() http.Handler {
function isAllowedToAccessMetricsEndpoint (line 32) | func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
FILE: internal/http/server/middleware.go
function middleware (line 16) | func middleware(next http.Handler) http.Handler {
FILE: internal/http/server/routes.go
function newRouter (line 18) | func newRouter(store *storage.Storage, pool *worker.Pool) http.Handler {
FILE: internal/integration/apprise/apprise.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
type Client (line 24) | type Client struct
method SendNotification (line 33) | func (c *Client) SendNotification(feed *model.Feed, entries model.Entr...
function NewClient (line 29) | func NewClient(serviceURL, baseURL string) *Client {
FILE: internal/integration/archiveorg/archiveorg.go
constant defaultClientTimeout (line 16) | defaultClientTimeout = 30 * time.Second
constant options (line 19) | options = "delay_wb_availability=1&if_not_archived_within=15d"
type Client (line 21) | type Client struct
method SendURL (line 27) | func (c *Client) SendURL(entryURL string) error {
function NewClient (line 23) | func NewClient() *Client {
FILE: internal/integration/betula/betula.go
constant defaultClientTimeout (line 19) | defaultClientTimeout = 10 * time.Second
type Client (line 21) | type Client struct
method CreateBookmark (line 30) | func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []st...
function NewClient (line 26) | func NewClient(url, token string) *Client {
FILE: internal/integration/cubox/cubox.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
type Client (line 24) | type Client struct
method SaveLink (line 32) | func (c *Client) SaveLink(entryURL string) error {
function NewClient (line 28) | func NewClient(apiLink string) *Client {
type card (line 69) | type card struct
FILE: internal/integration/discord/discord.go
constant defaultClientTimeout (line 23) | defaultClientTimeout = 10 * time.Second
constant discordMsgColor (line 24) | discordMsgColor = 5793266
type Client (line 26) | type Client struct
method SendDiscordMsg (line 34) | func (c *Client) SendDiscordMsg(feed *model.Feed, entries model.Entrie...
function NewClient (line 30) | func NewClient(webhookURL string) *Client {
type discordFields (line 97) | type discordFields struct
type discordEmbed (line 103) | type discordEmbed struct
type discordMessage (line 109) | type discordMessage struct
FILE: internal/integration/espial/espial.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
type Client (line 22) | type Client struct
method CreateLink (line 31) | func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) e...
function NewClient (line 27) | func NewClient(baseURL, apiKey string) *Client {
type espialDocument (line 78) | type espialDocument struct
FILE: internal/integration/instapaper/instapaper.go
constant defaultClientTimeout (line 17) | defaultClientTimeout = 10 * time.Second
type Client (line 19) | type Client struct
method AddURL (line 28) | func (c *Client) AddURL(entryURL, entryTitle string) error {
function NewClient (line 24) | func NewClient(username, password string) *Client {
FILE: internal/integration/integration.go
function SendEntry (line 41) | func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
function PushEntries (line 511) | func PushEntries(feed *model.Feed, entries model.Entries, userIntegratio...
FILE: internal/integration/integration_test.go
function TestSendEntryLogsLinkwardenCollectionID (line 15) | func TestSendEntryLogsLinkwardenCollectionID(t *testing.T) {
function TestSendEntryLogsLinkwardenWithoutCollectionID (line 41) | func TestSendEntryLogsLinkwardenWithoutCollectionID(t *testing.T) {
FILE: internal/integration/karakeep/karakeep.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
type Client (line 23) | type Client struct
method attachTags (line 56) | func (c *Client) attachTags(entryID string) error {
method SaveURL (line 110) | func (c *Client) SaveURL(entryURL string) error {
type tagItem (line 30) | type tagItem struct
type saveURLPayload (line 34) | type saveURLPayload struct
type saveURLResponse (line 39) | type saveURLResponse struct
type attachTagsPayload (line 43) | type attachTagsPayload struct
type errorResponse (line 47) | type errorResponse struct
function NewClient (line 52) | func NewClient(apiToken string, apiEndpoint string, tags string) *Client {
FILE: internal/integration/linkace/linkace.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
type Client (line 23) | type Client struct
method AddURL (line 35) | func (c *Client) AddURL(entryURL, entryTitle string) error {
function NewClient (line 31) | func NewClient(baseURL, apiKey, tags string, private bool, checkDisabled...
type createItemRequest (line 83) | type createItemRequest struct
FILE: internal/integration/linkding/linkding.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
type Client (line 23) | type Client struct
method CreateBookmark (line 34) | func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
function NewClient (line 30) | func NewClient(baseURL, apiKey, tags string, unread bool) *Client {
type linkdingBookmark (line 82) | type linkdingBookmark struct
FILE: internal/integration/linktaco/linktaco.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
constant defaultGraphQLURL (line 22) | defaultGraphQLURL = "https://api.linktaco.com/query"
constant maxTags (line 23) | maxTags = 10
constant maxDescriptionLength (line 24) | maxDescriptionLength = 500
type Client (line 27) | type Client struct
method CreateBookmark (line 48) | func (c *Client) CreateBookmark(entryURL, entryTitle, entryContent str...
function NewClient (line 35) | func NewClient(apiToken, orgSlug, tags, visibility string) *Client {
FILE: internal/integration/linktaco/linktaco_test.go
function TestCreateBookmark (line 17) | func TestCreateBookmark(t *testing.T) {
function TestNewClient (line 279) | func TestNewClient(t *testing.T) {
function TestGraphQLMutation (line 329) | func TestGraphQLMutation(t *testing.T) {
function BenchmarkCreateBookmark (line 402) | func BenchmarkCreateBookmark(b *testing.B) {
function BenchmarkTagProcessing (line 431) | func BenchmarkTagProcessing(b *testing.B) {
function configureIntegrationAllowPrivateNetworksOption (line 448) | func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
FILE: internal/integration/linkwarden/linkwarden.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
type Client (line 23) | type Client struct
method CreateBookmark (line 43) | func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
type linkwardenCollection (line 29) | type linkwardenCollection struct
type linkwardenRequest (line 33) | type linkwardenRequest struct
function NewClient (line 39) | func NewClient(baseURL, apiKey string, collectionID *int64) *Client {
FILE: internal/integration/linkwarden/linkwarden_test.go
function TestCreateBookmark (line 18) | func TestCreateBookmark(t *testing.T) {
function TestNewClient (line 282) | func TestNewClient(t *testing.T) {
function configureIntegrationAllowPrivateNetworksOption (line 330) | func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
FILE: internal/integration/matrixbot/client.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
type Client (line 22) | type Client struct
method DiscoverEndpoints (line 31) | func (c *Client) DiscoverEndpoints() (*DiscoveryEndpointResponse, erro...
method Login (line 65) | func (c *Client) Login(homeServerURL, matrixUsername, matrixPassword s...
method SendFormattedTextMessage (line 114) | func (c *Client) SendFormattedTextMessage(homeServerURL, accessToken, ...
function NewClient (line 26) | func NewClient(matrixBaseURL string) *Client {
type HomeServerInformation (line 162) | type HomeServerInformation struct
type IdentityServerInformation (line 166) | type IdentityServerInformation struct
type DiscoveryEndpointResponse (line 170) | type DiscoveryEndpointResponse struct
type UserIdentifier (line 175) | type UserIdentifier struct
type LoginRequest (line 180) | type LoginRequest struct
type LoginResponse (line 186) | type LoginResponse struct
type TextMessageEventRequest (line 193) | type TextMessageEventRequest struct
type RoomEventResponse (line 200) | type RoomEventResponse struct
FILE: internal/integration/matrixbot/matrixbot.go
function PushEntries (line 14) | func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL,...
FILE: internal/integration/notion/notion.go
constant defaultClientTimeout (line 18) | defaultClientTimeout = 10 * time.Second
type Client (line 20) | type Client struct
method UpdateDocument (line 29) | func (c *Client) UpdateDocument(entryURL string, entryTitle string) er...
function NewClient (line 25) | func NewClient(apiToken, pageID string) *Client {
type notionDocument (line 75) | type notionDocument struct
type block (line 79) | type block struct
type bookmarkObject (line 85) | type bookmarkObject struct
FILE: internal/integration/ntfy/ntfy.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
constant defaultNtfyURL (line 23) | defaultNtfyURL = "https://ntfy.sh"
type Client (line 26) | type Client struct
method SendMessages (line 48) | func (c *Client) SendMessages(feed *model.Feed, entries model.Entries)...
method makeRequest (line 87) | func (c *Client) makeRequest(payload any) error {
function NewClient (line 32) | func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassw...
type ntfyMessage (line 126) | type ntfyMessage struct
type ntfyAction (line 138) | type ntfyAction struct
FILE: internal/integration/nunuxkeeper/nunuxkeeper.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
type Client (line 22) | type Client struct
method AddEntry (line 31) | func (c *Client) AddEntry(entryURL, entryTitle, entryContent string) e...
function NewClient (line 27) | func NewClient(baseURL, apiKey string) *Client {
type nunuxKeeperDocument (line 74) | type nunuxKeeperDocument struct
FILE: internal/integration/omnivore/omnivore.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
constant defaultApiEndpoint (line 22) | defaultApiEndpoint = "https://api-prod.omnivore.app/api/graphql"
type errorResponse (line 39) | type errorResponse struct
type successResponse (line 45) | type successResponse struct
type Client (line 54) | type Client struct
method SaveURL (line 68) | func (c *Client) SaveURL(url string) error {
function NewClient (line 60) | func NewClient(apiToken string, apiEndpoint string) *Client {
FILE: internal/integration/pinboard/pinboard.go
constant defaultClientTimeout (line 21) | defaultClientTimeout = 10 * time.Second
type Client (line 23) | type Client struct
method CreateBookmark (line 31) | func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags str...
method getBookmark (line 78) | func (c *Client) getBookmark(entryURL string) (*Post, error) {
function NewClient (line 27) | func NewClient(authToken string) *Client {
FILE: internal/integration/pinboard/post.go
type Post (line 14) | type Post struct
method addTag (line 40) | func (p *Post) addTag(tag string) {
method SetToread (line 46) | func (p *Post) SetToread() {
method AddValues (line 50) | func (p *Post) AddValues(values url.Values) {
type posts (line 26) | type posts struct
function NewPost (line 31) | func NewPost(url string, description string) *Post {
FILE: internal/integration/pushover/pushover.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
constant defaultPushoverURL (line 23) | defaultPushoverURL = "https://api.pushover.net"
type Client (line 26) | type Client struct
method SendMessages (line 76) | func (c *Client) SendMessages(feed *model.Feed, entries model.Entries)...
method makeRequest (line 106) | func (c *Client) makeRequest(payload *message) error {
type message (line 36) | type message struct
type errorResponse (line 49) | type errorResponse struct
function NewClient (line 56) | func NewClient(user, token string, priority int, device, urlPrefix strin...
FILE: internal/integration/raindrop/raindrop.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
type Client (line 22) | type Client struct
method CreateRaindrop (line 33) | func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
function NewClient (line 28) | func NewClient(token, collectionID, tags string) *Client {
type raindrop (line 72) | type raindrop struct
type collection (line 79) | type collection struct
FILE: internal/integration/readeck/readeck.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
type Client (line 24) | type Client struct
method CreateBookmark (line 35) | func (c *Client) CreateBookmark(entryURL, entryTitle string, entryCont...
function NewClient (line 31) | func NewClient(baseURL, apiKey, labels string, onlyURL bool) *Client {
type readeckBookmark (line 139) | type readeckBookmark struct
type contentHeader (line 145) | type contentHeader struct
type partContentHeader (line 149) | type partContentHeader struct
FILE: internal/integration/readeck/readeck_test.go
function TestCreateBookmark (line 18) | func TestCreateBookmark(t *testing.T) {
function TestNewClient (line 245) | func TestNewClient(t *testing.T) {
function configureIntegrationAllowPrivateNetworksOption (line 266) | func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
FILE: internal/integration/readwise/readwise.go
constant readwiseApiEndpoint (line 22) | readwiseApiEndpoint = "https://readwise.io/api/v3/save/"
constant defaultClientTimeout (line 23) | defaultClientTimeout = 10 * time.Second
type Client (line 26) | type Client struct
method CreateDocument (line 34) | func (c *Client) CreateDocument(entryURL string) error {
function NewClient (line 30) | func NewClient(apiKey string) *Client {
type readwiseDocument (line 70) | type readwiseDocument struct
FILE: internal/integration/rssbridge/rssbridge.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 30 * time.Second
type Bridge (line 22) | type Bridge struct
type BridgeMeta (line 27) | type BridgeMeta struct
function DetectBridges (line 31) | func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*...
FILE: internal/integration/shaarli/shaarli.go
constant defaultClientTimeout (line 23) | defaultClientTimeout = 10 * time.Second
type Client (line 25) | type Client struct
method CreateLink (line 34) | func (c *Client) CreateLink(entryURL, entryTitle string) error {
method generateBearerToken (line 78) | func (c *Client) generateBearerToken() string {
function NewClient (line 30) | func NewClient(baseURL, apiSecret string) *Client {
type addLinkRequest (line 90) | type addLinkRequest struct
FILE: internal/integration/shiori/shiori.go
constant defaultClientTimeout (line 20) | defaultClientTimeout = 10 * time.Second
type Client (line 22) | type Client struct
method CreateBookmark (line 32) | func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
method authenticate (line 85) | func (c *Client) authenticate() (string, error) {
function NewClient (line 28) | func NewClient(baseURL, username, password string) *Client {
type authRequest (line 124) | type authRequest struct
type authResponse (line 130) | type authResponse struct
type authResponseMessage (line 135) | type authResponseMessage struct
type addBookmarkRequest (line 140) | type addBookmarkRequest struct
FILE: internal/integration/slack/slack.go
constant defaultClientTimeout (line 23) | defaultClientTimeout = 10 * time.Second
constant slackMsgColor (line 24) | slackMsgColor = "#5865F2"
type Client (line 26) | type Client struct
method SendSlackMsg (line 34) | func (c *Client) SendSlackMsg(feed *model.Feed, entries model.Entries)...
function NewClient (line 30) | func NewClient(webhookURL string) *Client {
type slackFields (line 101) | type slackFields struct
type slackAttachments (line 107) | type slackAttachments struct
type slackMessage (line 113) | type slackMessage struct
FILE: internal/integration/telegrambot/client.go
constant defaultClientTimeout (line 19) | defaultClientTimeout = 10 * time.Second
constant telegramAPIEndpoint (line 20) | telegramAPIEndpoint = "https://api.telegram.org"
constant MarkdownFormatting (line 22) | MarkdownFormatting = "Markdown"
constant MarkdownV2Formatting (line 23) | MarkdownV2Formatting = "MarkdownV2"
constant HTMLFormatting (line 24) | HTMLFormatting = "HTML"
type Client (line 27) | type Client struct
method GetMe (line 40) | func (c *Client) GetMe() (*User, error) {
method SendMessage (line 74) | func (c *Client) SendMessage(message *MessageRequest) (*Message, error) {
function NewClient (line 32) | func NewClient(botToken, chatID string) *Client {
type InlineKeyboard (line 113) | type InlineKeyboard struct
type InlineKeyboardRow (line 117) | type InlineKeyboardRow
type InlineKeyboardButton (line 119) | type InlineKeyboardButton struct
type User (line 124) | type User struct
type Chat (line 137) | type Chat struct
type Message (line 143) | type Message struct
type BaseResponse (line 151) | type BaseResponse struct
type UserResponse (line 157) | type UserResponse struct
type MessageRequest (line 162) | type MessageRequest struct
type MessageResponse (line 172) | type MessageResponse struct
FILE: internal/integration/telegrambot/telegrambot.go
function PushEntry (line 16) | func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID st...
FILE: internal/integration/wallabag/wallabag.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
type Client (line 24) | type Client struct
method CreateEntry (line 46) | func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string...
method createEntry (line 59) | func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryC...
method getAccessToken (line 103) | func (c *Client) getAccessToken() (string, error) {
function NewClient (line 34) | func NewClient(baseURL, clientID, clientSecret, username, password, tags...
type tokenResponse (line 144) | type tokenResponse struct
type createEntryRequest (line 152) | type createEntryRequest struct
FILE: internal/integration/wallabag/wallabag_test.go
function TestCreateEntry (line 17) | func TestCreateEntry(t *testing.T) {
function TestNewClient (line 275) | func TestNewClient(t *testing.T) {
function configureIntegrationAllowPrivateNetworksOption (line 327) | func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
FILE: internal/integration/webhook/webhook.go
constant defaultClientTimeout (line 22) | defaultClientTimeout = 10 * time.Second
constant NewEntriesEventType (line 24) | NewEntriesEventType = "new_entries"
constant SaveEntryEventType (line 25) | SaveEntryEventType = "save_entry"
type Client (line 28) | type Client struct
method SendSaveEntryWebhookEvent (line 37) | func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
method SendNewEntriesWebhookEvent (line 73) | func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries ...
method makeRequest (line 117) | func (c *Client) makeRequest(eventType string, payload any) error {
function NewClient (line 33) | func NewClient(webhookURL, webhookSecret string) *Client {
type WebhookFeed (line 151) | type WebhookFeed struct
type WebhookCategory (line 162) | type WebhookCategory struct
type WebhookEntry (line 167) | type WebhookEntry struct
type WebhookNewEntriesEvent (line 189) | type WebhookNewEntriesEvent struct
type WebhookSaveEntryEvent (line 195) | type WebhookSaveEntryEvent struct
FILE: internal/locale/catalog.go
type translationDict (line 12) | type translationDict struct
method UnmarshalJSON (line 47) | func (t *translationDict) UnmarshalJSON(data []byte) error {
type catalog (line 16) | type catalog
function getTranslationDict (line 23) | func getTranslationDict(language string) (translationDict, error) {
function loadTranslationFile (line 33) | func loadTranslationFile(language string) (translationDict, error) {
function parseTranslationMessages (line 81) | func parseTranslationMessages(data []byte) (translationDict, error) {
FILE: internal/locale/catalog_test.go
function TestParserWithInvalidData (line 10) | func TestParserWithInvalidData(t *testing.T) {
function TestParser (line 17) | func TestParser(t *testing.T) {
function TestLoadCatalog (line 33) | func TestLoadCatalog(t *testing.T) {
function TestAllKeysHaveValue (line 42) | func TestAllKeysHaveValue(t *testing.T) {
function TestMissingTranslations (line 70) | func TestMissingTranslations(t *testing.T) {
function TestTranslationFilePluralForms (line 100) | func TestTranslationFilePluralForms(t *testing.T) {
FILE: internal/locale/error.go
type LocalizedErrorWrapper (line 8) | type LocalizedErrorWrapper struct
method Error (line 22) | func (l *LocalizedErrorWrapper) Error() error {
method Translate (line 26) | func (l *LocalizedErrorWrapper) Translate(language string) string {
function NewLocalizedErrorWrapper (line 14) | func NewLocalizedErrorWrapper(originalErr error, translationKey string, ...
type LocalizedError (line 36) | type LocalizedError struct
method String (line 45) | func (v *LocalizedError) String() string {
method Error (line 49) | func (v *LocalizedError) Error() error {
method Translate (line 53) | func (v *LocalizedError) Translate(language string) string {
function NewLocalizedError (line 41) | func NewLocalizedError(translationKey string, translationArgs ...any) *L...
FILE: internal/locale/error_test.go
function TestNewLocalizedErrorWrapper (line 11) | func TestNewLocalizedErrorWrapper(t *testing.T) {
function TestLocalizedErrorWrapper_Error (line 35) | func TestLocalizedErrorWrapper_Error(t *testing.T) {
function TestLocalizedErrorWrapper_Translate (line 45) | func TestLocalizedErrorWrapper_Translate(t *testing.T) {
function TestLocalizedErrorWrapper_TranslateWithEmptyKey (line 85) | func TestLocalizedErrorWrapper_TranslateWithEmptyKey(t *testing.T) {
function TestLocalizedErrorWrapper_TranslateWithNoArgs (line 96) | func TestLocalizedErrorWrapper_TranslateWithNoArgs(t *testing.T) {
function TestNewLocalizedError (line 115) | func TestNewLocalizedError(t *testing.T) {
function TestLocalizedError_String (line 134) | func TestLocalizedError_String(t *testing.T) {
function TestLocalizedError_StringWithMissingTranslation (line 152) | func TestLocalizedError_StringWithMissingTranslation(t *testing.T) {
function TestLocalizedError_Error (line 166) | func TestLocalizedError_Error(t *testing.T) {
function TestLocalizedError_Translate (line 188) | func TestLocalizedError_Translate(t *testing.T) {
function TestLocalizedError_TranslateWithNoArgs (line 226) | func TestLocalizedError_TranslateWithNoArgs(t *testing.T) {
function TestLocalizedError_TranslateWithComplexArgs (line 257) | func TestLocalizedError_TranslateWithComplexArgs(t *testing.T) {
function TestLocalizedErrorWrapper_WithNilError (line 275) | func TestLocalizedErrorWrapper_WithNilError(t *testing.T) {
function TestLocalizedErrorWrapper_TranslateWithEmptyKeyAndNilError (line 286) | func TestLocalizedErrorWrapper_TranslateWithEmptyKeyAndNilError(t *testi...
function TestLocalizedError_EmptyKey (line 296) | func TestLocalizedError_EmptyKey(t *testing.T) {
FILE: internal/locale/locale_test.go
function TestAvailableLanguages (line 8) | func TestAvailableLanguages(t *testing.T) {
FILE: internal/locale/plural.go
function getPluralForm (line 8) | func getPluralForm(lang string, n int) int {
FILE: internal/locale/plural_test.go
function TestPluralRules (line 8) | func TestPluralRules(t *testing.T) {
FILE: internal/locale/printer.go
type Printer (line 9) | type Printer struct
method Print (line 18) | func (p *Printer) Print(key string) string {
method Printf (line 28) | func (p *Printer) Printf(key string, args ...any) string {
method Plural (line 33) | func (p *Printer) Plural(key string, n int, args ...any) string {
function NewPrinter (line 14) | func NewPrinter(language string) *Printer {
FILE: internal/locale/printer_test.go
function TestPrintfWithMissingLanguage (line 8) | func TestPrintfWithMissingLanguage(t *testing.T) {
function TestPrintfWithMissingKey (line 17) | func TestPrintfWithMissingKey(t *testing.T) {
function TestPrintfWithExistingKey (line 32) | func TestPrintfWithExistingKey(t *testing.T) {
function TestPrintfWithExistingKeyAndPlaceholder (line 47) | func TestPrintfWithExistingKeyAndPlaceholder(t *testing.T) {
function TestPrintfWithMissingKeyAndPlaceholder (line 67) | func TestPrintfWithMissingKeyAndPlaceholder(t *testing.T) {
function TestPrintWithMissingLanguage (line 87) | func TestPrintWithMissingLanguage(t *testing.T) {
function TestPrintWithMissingKey (line 96) | func TestPrintWithMissingKey(t *testing.T) {
function TestPrintWithExistingKey (line 111) | func TestPrintWithExistingKey(t *testing.T) {
function TestPrintWithDifferentLanguages (line 126) | func TestPrintWithDifferentLanguages(t *testing.T) {
function TestPrintWithEmptyKey (line 162) | func TestPrintWithEmptyKey(t *testing.T) {
function TestPrintWithEmptyTranslation (line 177) | func TestPrintWithEmptyTranslation(t *testing.T) {
function TestPluralWithDefaultRule (line 192) | func TestPluralWithDefaultRule(t *testing.T) {
function TestPluralWithRussianRule (line 220) | func TestPluralWithRussianRule(t *testing.T) {
function TestPluralWithMissingTranslation (line 255) | func TestPluralWithMissingTranslation(t *testing.T) {
function TestPluralWithMissingLanguage (line 271) | func TestPluralWithMissingLanguage(t *testing.T) {
function TestPluralWithIndexOutOfBounds (line 280) | func TestPluralWithIndexOutOfBounds(t *testing.T) {
function TestPluralWithVariousLanguageRules (line 306) | func TestPluralWithVariousLanguageRules(t *testing.T) {
FILE: internal/mediaproxy/media_proxy_test.go
function TestRewriteDocumentWithRelativeProxyURL_None_Image (line 13) | func TestRewriteDocumentWithRelativeProxyURL_None_Image(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_None_Audio (line 35) | func TestRewriteDocumentWithRelativeProxyURL_None_Audio(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_None_Video (line 57) | func TestRewriteDocumentWithRelativeProxyURL_None_Video(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_None_VideoPoster (line 79) | func TestRewriteDocumentWithRelativeProxyURL_None_VideoPoster(t *testing...
function TestRewriteDocumentWithAbsoluteProxyURL_None_Image (line 101) | func TestRewriteDocumentWithAbsoluteProxyURL_None_Image(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_None_Audio (line 123) | func TestRewriteDocumentWithAbsoluteProxyURL_None_Audio(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_None_Video (line 145) | func TestRewriteDocumentWithAbsoluteProxyURL_None_Video(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_None_VideoPoster (line 167) | func TestRewriteDocumentWithAbsoluteProxyURL_None_VideoPoster(t *testing...
function TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Image (line 189) | func TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Image(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Audio (line 211) | func TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Audio(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Video (line 233) | func TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Video(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_HttpOnly_VideoPoster (line 255) | func TestRewriteDocumentWithRelativeProxyURL_HttpOnly_VideoPoster(t *tes...
function TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Image_Poster (line 277) | func TestRewriteDocumentWithRelativeProxyURL_HttpOnly_Image_Poster(t *te...
function TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Image (line 299) | func TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Image(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Audio (line 321) | func TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Audio(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Video (line 343) | func TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_Video(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_VideoPoster (line 365) | func TestRewriteDocumentWithAbsoluteProxyURL_HttpOnly_VideoPoster(t *tes...
function TestRewriteDocumentWithRelativeProxyURL_All_Image (line 387) | func TestRewriteDocumentWithRelativeProxyURL_All_Image(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_All_Audio (line 409) | func TestRewriteDocumentWithRelativeProxyURL_All_Audio(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_All_Video (line 432) | func TestRewriteDocumentWithRelativeProxyURL_All_Video(t *testing.T) {
function TestRewriteDocumentWithRelativeProxyURL_All_VideoPoster (line 454) | func TestRewriteDocumentWithRelativeProxyURL_All_VideoPoster(t *testing....
function TestRewriteDocumentWithAbsoluteProxyURL_All_Image (line 476) | func TestRewriteDocumentWithAbsoluteProxyURL_All_Image(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_All_Audio (line 498) | func TestRewriteDocumentWithAbsoluteProxyURL_All_Audio(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_All_Video (line 520) | func TestRewriteDocumentWithAbsoluteProxyURL_All_Video(t *testing.T) {
function TestRewriteDocumentWithAbsoluteProxyURL_All_VideoPoster (line 542) | func TestRewriteDocumentWithAbsoluteProxyURL_All_VideoPoster(t *testing....
function TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Image (line 564) | func TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Image(t *testi...
function TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Audio (line 587) | func TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Audio(t *testi...
function TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Video (line 610) | func TestRewriteDocumentWithRelativeProxyURL_BasePath_All_Video(t *testi...
function TestRewriteDocumentWithRelativeProxyURL_BasePath_All_VideoPoster (line 633) | func TestRewriteDocumentWithRelativeProxyURL_BasePath_All_VideoPoster(t ...
function TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Image (line 656) | func TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Image(t *testi...
function TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Audio (line 679) | func TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Audio(t *testi...
function TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Video (line 702) | func TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_Video(t *testi...
function TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_VideoPoster (line 725) | func TestRewriteDocumentWithAbsoluteProxyURL_BasePath_All_VideoPoster(t ...
function TestRewriteDocumentWithRelativeProxyURL_CustomMediaProxy_All_Image (line 748) | func TestRewriteDocumentWithRelativeProxyURL_CustomMediaProxy_All_Image(...
function TestRewriteDocumentWithAbsoluteProxyURL_CustomMediaProxy_All_Image (line 771) | func TestRewriteDocumentWithAbsoluteProxyURL_CustomMediaProxy_All_Image(...
function TestMediaProxyWithIncorrectCustomMediaProxy (line 794) | func TestMediaProxyWithIncorrectCustomMediaProxy(t *testing.T) {
function TestMediaProxyFilterWithImageSrcset (line 808) | func TestMediaProxyFilterWithImageSrcset(t *testing.T) {
function TestMediaProxyFilterWithEmptySrcset (line 830) | func TestMediaProxyFilterWithEmptySrcset(t *testing.T) {
function TestProxyFilterWithPictureSource (line 852) | func TestProxyFilterWithPictureSource(t *testing.T) {
function TestProxyFilterOnlyNonHTTPWithPictureSource (line 874) | func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
function TestMediaProxyWithImageDataURL (line 896) | func TestMediaProxyWithImageDataURL(t *testing.T) {
function TestMediaProxyWithImageSourceDataURL (line 917) | func TestMediaProxyWithImageSourceDataURL(t *testing.T) {
function TestShouldProxifyURLWithMimeType (line 938) | func TestShouldProxifyURLWithMimeType(t *testing.T) {
FILE: internal/mediaproxy/rewriter.go
type urlProxyRewriter (line 17) | type urlProxyRewriter
function RewriteDocumentWithRelativeProxyURL (line 19) | func RewriteDocumentWithRelativeProxyURL(htmlDocument string) string {
function RewriteDocumentWithAbsoluteProxyURL (line 23) | func RewriteDocumentWithAbsoluteProxyURL(htmlDocument string) string {
function genericProxyRewriter (line 27) | func genericProxyRewriter(proxifyFunction urlProxyRewriter, htmlDocument...
function proxifySourceSet (line 97) | func proxifySourceSet(element *goquery.Selection, proxifyFunction urlPro...
function shouldProxifyURL (line 110) | func shouldProxifyURL(mediaURL, mediaProxyOption string) bool {
function ShouldProxifyURLWithMimeType (line 127) | func ShouldProxifyURLWithMimeType(mediaURL, mediaMimeType, mediaProxyOpt...
FILE: internal/mediaproxy/url.go
function ProxifyRelativeURL (line 16) | func ProxifyRelativeURL(mediaURL string) string {
function ProxifyAbsoluteURL (line 35) | func ProxifyAbsoluteURL(mediaURL string) string {
function proxifyURLWithCustomProxy (line 54) | func proxifyURLWithCustomProxy(mediaURL string, customProxyURL *url.URL)...
FILE: internal/metric/metric.go
type collector (line 139) | type collector struct
method GatherStorageMetrics (line 165) | func (c *collector) GatherStorageMetrics() {
function NewCollector (line 145) | func NewCollector(store *storage.Storage, refreshInterval time.Duration)...
FILE: internal/model/api_key.go
type APIKey (line 13) | type APIKey struct
type APIKeys (line 23) | type APIKeys
type APIKeyCreationRequest (line 26) | type APIKeyCreationRequest struct
FILE: internal/model/app_session.go
type SessionData (line 14) | type SessionData struct
method String (line 26) | func (s *SessionData) String() string {
method Value (line 41) | func (s *SessionData) Value() (driver.Value, error) {
method Scan (line 47) | func (s *SessionData) Scan(src any) error {
type Session (line 62) | type Session struct
method String (line 67) | func (s *Session) String() string {
FILE: internal/model/categories_sort_options.go
function CategoriesSortingOptions (line 6) | func CategoriesSortingOptions() map[string]string {
FILE: internal/model/category.go
type Category (line 9) | type Category struct
method String (line 19) | func (c *Category) String() string {
type CategoryCreationRequest (line 23) | type CategoryCreationRequest struct
type CategoryModificationRequest (line 28) | type CategoryModificationRequest struct
method Patch (line 33) | func (c *CategoryModificationRequest) Patch(category *Category) {
type Categories (line 44) | type Categories
FILE: internal/model/enclosure.go
type Enclosure (line 13) | type Enclosure struct
method Html5MimeType (line 28) | func (e *Enclosure) Html5MimeType() string {
method IsAudio (line 35) | func (e *Enclosure) IsAudio() bool {
method IsVideo (line 39) | func (e *Enclosure) IsVideo() bool {
method IsImage (line 43) | func (e *Enclosure) IsImage() bool {
method ProxifyEnclosureURL (line 53) | func (e *Enclosure) ProxifyEnclosureURL(mediaProxyOption string, media...
type EnclosureUpdateRequest (line 23) | type EnclosureUpdateRequest struct
type EnclosureList (line 60) | type EnclosureList
method FindMediaPlayerEnclosure (line 63) | func (el EnclosureList) FindMediaPlayerEnclosure() *Enclosure {
method ContainsAudioOrVideo (line 75) | func (el EnclosureList) ContainsAudioOrVideo() bool {
method ProxifyEnclosureURL (line 84) | func (el EnclosureList) ProxifyEnclosureURL(mediaProxyOption string, m...
FILE: internal/model/enclosure_test.go
function TestEnclosure_Html5MimeTypeGivesOriginalMimeType (line 13) | func TestEnclosure_Html5MimeTypeGivesOriginalMimeType(t *testing.T) {
function TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeType (line 24) | func TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeTyp...
function TestEnclosure_IsAudio (line 38) | func TestEnclosure_IsAudio(t *testing.T) {
function TestEnclosure_IsVideo (line 65) | func TestEnclosure_IsVideo(t *testing.T) {
function TestEnclosure_IsImage (line 93) | func TestEnclosure_IsImage(t *testing.T) {
function TestEnclosureList_FindMediaPlayerEnclosure (line 128) | func TestEnclosureList_FindMediaPlayerEnclosure(t *testing.T) {
function TestEnclosureList_ContainsAudioOrVideo (line 207) | func TestEnclosureList_ContainsAudioOrVideo(t *testing.T) {
function TestEnclosure_ProxifyEnclosureURL (line 284) | func TestEnclosure_ProxifyEnclosureURL(t *testing.T) {
function TestEnclosureList_ProxifyEnclosureURL (line 411) | func TestEnclosureList_ProxifyEnclosureURL(t *testing.T) {
function TestEnclosure_ProxifyEnclosureURL_EdgeCases (line 523) | func TestEnclosure_ProxifyEnclosureURL_EdgeCases(t *testing.T) {
FILE: internal/model/entry.go
constant EntryStatusUnread (line 12) | EntryStatusUnread = "unread"
constant EntryStatusRead (line 13) | EntryStatusRead = "read"
constant EntryStatusRemoved (line 14) | EntryStatusRemoved = "removed"
constant DefaultSortingOrder (line 15) | DefaultSortingOrder = "published_at"
constant DefaultSortingDirection (line 16) | DefaultSortingDirection = "asc"
type Entry (line 20) | type Entry struct
method ShouldMarkAsReadOnView (line 54) | func (e *Entry) ShouldMarkAsReadOnView(user *User) bool {
function NewEntry (line 42) | func NewEntry() *Entry {
type Entries (line 70) | type Entries
type EntriesStatusUpdateRequest (line 73) | type EntriesStatusUpdateRequest struct
type EntryUpdateRequest (line 79) | type EntryUpdateRequest struct
method Patch (line 84) | func (e *EntryUpdateRequest) Patch(entry *Entry) {
FILE: internal/model/feed.go
constant SchedulerRoundRobin (line 16) | SchedulerRoundRobin = "round_robin"
constant SchedulerEntryFrequency (line 17) | SchedulerEntryFrequency = "entry_frequency"
constant DefaultFeedSorting (line 19) | DefaultFeedSorting = "parsing_error_count"
constant DefaultFeedSortingDirection (line 20) | DefaultFeedSortingDirection = "desc"
type Feed (line 24) | type Feed struct
method String (line 84) | func (f *Feed) String() string {
method WithCategoryID (line 96) | func (f *Feed) WithCategoryID(categoryID int64) {
method WithTranslatedErrorMessage (line 101) | func (f *Feed) WithTranslatedErrorMessage(message string) {
method ResetErrorCounter (line 107) | func (f *Feed) ResetErrorCounter() {
method CheckedNow (line 113) | func (f *Feed) CheckedNow() {
method ScheduleNextCheck (line 122) | func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelay time.Du...
type FeedCounters (line 79) | type FeedCounters struct
type FeedCreationRequest (line 152) | type FeedCreationRequest struct
type FeedCreationRequestFromSubscriptionDiscovery (line 178) | type FeedCreationRequestFromSubscriptionDiscovery struct
type FeedModificationRequest (line 187) | type FeedModificationRequest struct
method Patch (line 217) | func (f *FeedModificationRequest) Patch(feed *Feed) {
type Feeds (line 324) | type Feeds
FILE: internal/model/feed_test.go
constant largeWeeklyCount (line 16) | largeWeeklyCount = 10080
constant noRefreshDelay (line 17) | noRefreshDelay = 0
function TestFeedCategorySetter (line 20) | func TestFeedCategorySetter(t *testing.T) {
function TestFeedErrorCounter (line 33) | func TestFeedErrorCounter(t *testing.T) {
function TestFeedCheckedNow (line 56) | func TestFeedCheckedNow(t *testing.T) {
function checkTargetInterval (line 70) | func checkTargetInterval(t *testing.T, feed *Feed, targetInterval time.D...
function TestFeedScheduleNextCheckRoundRobinDefault (line 79) | func TestFeedScheduleNextCheckRoundRobinDefault(t *testing.T) {
function TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval (line 101) | func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval...
function TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval (line 124) | func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval...
function TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval (line 147) | func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval...
function TestFeedScheduleNextCheckRoundRobinMinInterval (line 170) | func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
function TestFeedScheduleNextCheckEntryFrequencyMaxInterval (line 195) | func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) {
function TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount (line 224) | func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t...
function TestFeedScheduleNextCheckEntryFrequencyMinInterval (line 253) | func TestFeedScheduleNextCheckEntryFrequencyMinInterval(t *testing.T) {
function TestFeedScheduleNextCheckEntryFrequencyFactor (line 282) | func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) {
function TestFeedScheduleNextCheckEntryFrequencySmallNewTTL (line 308) | func TestFeedScheduleNextCheckEntryFrequencySmallNewTTL(t *testing.T) {
function TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL (line 344) | func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) {
FILE: internal/model/home_page.go
function HomePages (line 7) | func HomePages() map[string]string {
FILE: internal/model/icon.go
type Icon (line 11) | type Icon struct
method DataURL (line 20) | func (i *Icon) DataURL() string {
type Icons (line 25) | type Icons
type FeedIcon (line 28) | type FeedIcon struct
FILE: internal/model/integration.go
type Integration (line 7) | type Integration struct
FILE: internal/model/job.go
type Job (line 7) | type Job struct
type JobList (line 14) | type JobList
method FeedURLs (line 18) | func (jl *JobList) FeedURLs() []string {
FILE: internal/model/model.go
type Number (line 6) | type Number interface
function OptionalNumber (line 10) | func OptionalNumber[T Number](value T) *T {
function OptionalString (line 17) | func OptionalString(value string) *string {
function SetOptionalField (line 25) | func SetOptionalField[T any](value T) *T {
FILE: internal/model/subscription.go
type SubscriptionDiscoveryRequest (line 7) | type SubscriptionDiscoveryRequest struct
FILE: internal/model/theme.go
function Themes (line 7) | func Themes() map[string]string {
function ThemeColor (line 22) | func ThemeColor(theme, colorScheme string) string {
FILE: internal/model/user.go
type User (line 13) | type User struct
method UseTimezone (line 215) | func (u *User) UseTimezone(tz string) {
type UserCreationRequest (line 49) | type UserCreationRequest struct
type UserModificationRequest (line 58) | type UserModificationRequest struct
method Patch (line 92) | func (u *UserModificationRequest) Patch(user *User) {
type Users (line 222) | type Users
method UseTimezone (line 225) | func (u Users) UseTimezone(tz string) {
FILE: internal/model/user_session.go
type UserSession (line 14) | type UserSession struct
method String (line 23) | func (u *UserSession) String() string {
method UseTimezone (line 28) | func (u *UserSession) UseTimezone(tz string) {
FILE: internal/model/webauthn.go
type WebAuthnSession (line 18) | type WebAuthnSession struct
method Value (line 22) | func (s WebAuthnSession) Value() (driver.Value, error) {
method Scan (line 26) | func (s *WebAuthnSession) Scan(value any) error {
method String (line 35) | func (s WebAuthnSession) String() string {
type WebAuthnCredential (line 42) | type WebAuthnCredential struct
method HandleEncoded (line 50) | func (s WebAuthnCredential) HandleEncoded() string {
FILE: internal/oauth2/authorization.go
type Authorization (line 15) | type Authorization struct
method RedirectURL (line 21) | func (u *Authorization) RedirectURL() string {
method State (line 25) | func (u *Authorization) State() string {
method CodeVerifier (line 29) | func (u *Authorization) CodeVerifier() string {
function GenerateAuthorization (line 33) | func GenerateAuthorization(config *oauth2.Config) *Authorization {
FILE: internal/oauth2/google.go
type googleProfile (line 16) | type googleProfile struct
type googleProvider (line 21) | type googleProvider struct
method GetConfig (line 31) | func (g *googleProvider) GetConfig() *oauth2.Config {
method GetUserExtraKey (line 44) | func (g *googleProvider) GetUserExtraKey() string {
method GetProfile (line 48) | func (g *googleProvider) GetProfile(ctx context.Context, code, codeVer...
method PopulateUserCreationWithProfileID (line 72) | func (g *googleProvider) PopulateUserCreationWithProfileID(user *model...
method PopulateUserWithProfileID (line 76) | func (g *googleProvider) PopulateUserWithProfileID(user *model.User, p...
method UnsetUserProfileID (line 80) | func (g *googleProvider) UnsetUserProfileID(user *model.User) {
function NewGoogleProvider (line 27) | func NewGoogleProvider(clientID, clientSecret, redirectURL string) *goog...
FILE: internal/oauth2/manager.go
type Manager (line 12) | type Manager struct
method FindProvider (line 16) | func (m *Manager) FindProvider(name string) (Provider, error) {
method AddProvider (line 24) | func (m *Manager) AddProvider(name string, provider Provider) {
function NewManager (line 28) | func NewManager(ctx context.Context, clientID, clientSecret, redirectURL...
FILE: internal/oauth2/oidc.go
type oidcProvider (line 21) | type oidcProvider struct
method GetUserExtraKey (line 42) | func (o *oidcProvider) GetUserExtraKey() string {
method GetConfig (line 46) | func (o *oidcProvider) GetConfig() *oauth2.Config {
method GetProfile (line 56) | func (o *oidcProvider) GetProfile(ctx context.Context, code, codeVerif...
method PopulateUserCreationWithProfileID (line 94) | func (o *oidcProvider) PopulateUserCreationWithProfileID(user *model.U...
method PopulateUserWithProfileID (line 98) | func (o *oidcProvider) PopulateUserWithProfileID(user *model.User, pro...
method UnsetUserProfileID (line 102) | func (o *oidcProvider) UnsetUserProfileID(user *model.User) {
function NewOidcProvider (line 28) | func NewOidcProvider(ctx context.Context, clientID, clientSecret, redire...
type userClaims (line 106) | type userClaims struct
FILE: internal/oauth2/profile.go
type Profile (line 11) | type Profile struct
method String (line 17) | func (p Profile) String() string {
FILE: internal/oauth2/provider.go
type Provider (line 15) | type Provider interface
FILE: internal/proxyrotator/proxyrotator.go
type ProxyRotator (line 14) | type ProxyRotator struct
method GetNextProxy (line 40) | func (pr *ProxyRotator) GetNextProxy() *url.URL {
method HasProxies (line 54) | func (pr *ProxyRotator) HasProxies() bool {
function NewProxyRotator (line 21) | func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
FILE: internal/proxyrotator/proxyrotator_test.go
function TestProxyRotator (line 10) | func TestProxyRotator(t *testing.T) {
function TestProxyRotatorEmpty (line 41) | func TestProxyRotatorEmpty(t *testing.T) {
function TestProxyRotatorInvalidURL (line 57) | func TestProxyRotatorInvalidURL(t *testing.T) {
FILE: internal/reader/atom/atom_03.go
type atom03Feed (line 13) | type atom03Feed struct
type atom03Entry (line 44) | type atom03Entry struct
type atom03Content (line 93) | type atom03Content struct
method content (line 116) | func (a *atom03Content) content() string {
FILE: internal/reader/atom/atom_03_adapter.go
type atom03Adapter (line 17) | type atom03Adapter struct
method buildFeed (line 21) | func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
FILE: internal/reader/atom/atom_03_test.go
function TestParseAtom03 (line 12) | func TestParseAtom03(t *testing.T) {
function TestParseAtom03WithoutSiteURL (line 77) | func TestParseAtom03WithoutSiteURL(t *testing.T) {
function TestParseAtom03WithoutFeedTitle (line 99) | func TestParseAtom03WithoutFeedTitle(t *testing.T) {
function TestParseAtom03WithoutEntryTitleButWithLink (line 122) | func TestParseAtom03WithoutEntryTitleButWithLink(t *testing.T) {
function TestParseAtom03WithoutEntryTitleButWithSummary (line 149) | func TestParseAtom03WithoutEntryTitleButWithSummary(t *testing.T) {
function TestParseAtom03WithoutEntryTitleButWithXMLContent (line 177) | func TestParseAtom03WithoutEntryTitleButWithXMLContent(t *testing.T) {
function TestParseAtom03WithSummaryOnly (line 205) | func TestParseAtom03WithSummaryOnly(t *testing.T) {
function TestParseAtom03WithXMLContent (line 236) | func TestParseAtom03WithXMLContent(t *testing.T) {
function TestParseAtom03WithBase64Content (line 267) | func TestParseAtom03WithBase64Content(t *testing.T) {
FILE: internal/reader/atom/atom_10.go
type atom10Feed (line 22) | type atom10Feed struct
type atom10Entry (line 87) | type atom10Entry struct
type atom10Text (line 163) | type atom10Text struct
method body (line 170) | func (a *atom10Text) body() string {
method title (line 182) | func (a *atom10Text) title() string {
method xhtmlContent (line 197) | func (a *atom10Text) xhtmlContent() string {
type atomXHTMLRootElement (line 204) | type atomXHTMLRootElement struct
FILE: internal/reader/atom/atom_10_adapter.go
type atom10Adapter (line 21) | type atom10Adapter struct
method BuildFeed (line 29) | func (a *atom10Adapter) BuildFeed(baseURL string) *model.Feed {
method populateEntries (line 75) | func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
function NewAtom10Adapter (line 25) | func NewAtom10Adapter(atomFeed *atom10Feed) *atom10Adapter {
FILE: internal/reader/atom/atom_10_test.go
function TestParseAtomSample (line 12) | func TestParseAtomSample(t *testing.T) {
function TestParseFeedWithSubtitle (line 85) | func TestParseFeedWithSubtitle(t *testing.T) {
function TestParseFeedWithoutTitle (line 108) | func TestParseFeedWithoutTitle(t *testing.T) {
function TestParseEntryWithoutTitleButWithURL (line 126) | func TestParseEntryWithoutTitleButWithURL(t *testing.T) {
function TestParseEntryWithoutTitleButWithSummary (line 156) | func TestParseEntryWithoutTitleButWithSummary(t *testing.T) {
function TestParseEntryWithoutTitleButWithXHTMLContent (line 187) | func TestParseEntryWithoutTitleButWithXHTMLContent(t *testing.T) {
function TestParseFeedURL (line 220) | func TestParseFeedURL(t *testing.T) {
function TestParseFeedWithRelativeFeedURL (line 243) | func TestParseFeedWithRelativeFeedURL(t *testing.T) {
function TestParseFeedWithRelativeSiteURL (line 262) | func TestParseFeedWithRelativeSiteURL(t *testing.T) {
function TestParseFeedSiteURLWithTrailingSpace (line 298) | func TestParseFeedSiteURLWithTrailingSpace(t *testing.T) {
function TestParseFeedWithFeedURLWithTrailingSpace (line 314) | func TestParseFeedWithFeedURLWithTrailingSpace(t *testing.T) {
function TestParseEntryWithRelativeURL (line 330) | func TestParseEntryWithRelativeURL(t *testing.T) {
function TestParseEntryURLWithTextHTMLType (line 356) | func TestParseEntryURLWithTextHTMLType(t *testing.T) {
function TestParseEntryURLWithNoRelAndNoType (line 382) | func TestParseEntryURLWithNoRelAndNoType(t *testing.T) {
function TestParseEntryURLWithAlternateRel (line 408) | func TestParseEntryURLWithAlternateRel(t *testing.T) {
function TestParseEntryTitleWithWhitespaces (line 434) | func TestParseEntryTitleWithWhitespaces(t *testing.T) {
function TestParseEntryWithPlainTextTitle (line 462) | func TestParseEntryWithPlainTextTitle(t *testing.T) {
function TestParseEntryWithHTMLTitle (line 499) | func TestParseEntryWithHTMLTitle(t *testing.T) {
function TestParseEntryWithXHTMLTitle (line 550) | func TestParseEntryWithXHTMLTitle(t *testing.T) {
function TestParseEntryWithEmptyXHTMLTitle (line 580) | func TestParseEntryWithEmptyXHTMLTitle(t *testing.T) {
function TestParseEntryWithXHTMLTitleWithoutDiv (line 607) | func TestParseEntryWithXHTMLTitleWithoutDiv(t *testing.T) {
function TestParseEntryWithNumericCharacterReferenceTitle (line 634) | func TestParseEntryWithNumericCharacterReferenceTitle(t *testing.T) {
function TestParseEntryWithDoubleEncodedEntitiesTitle (line 660) | func TestParseEntryWithDoubleEncodedEntitiesTitle(t *testing.T) {
function TestParseEntryWithXHTMLSummary (line 686) | func TestParseEntryWithXHTMLSummary(t *testing.T) {
function TestParseEntryWithHTMLSummary (line 712) | func TestParseEntryWithHTMLSummary(t *testing.T) {
function TestParseEntryWithTextSummary (line 751) | func TestParseEntryWithTextSummary(t *testing.T) {
function TestParseEntryWithTextContent (line 803) | func TestParseEntryWithTextContent(t *testing.T) {
function TestParseEntryWithHTMLContent (line 856) | func TestParseEntryWithHTMLContent(t *testing.T) {
function TestParseEntryWithXHTMLContent (line 901) | func TestParseEntryWithXHTMLContent(t *testing.T) {
function TestParseEntryWithAuthorName (line 929) | func TestParseEntryWithAuthorName(t *testing.T) {
function TestParseEntryWithoutAuthorName (line 958) | func TestParseEntryWithoutAuthorName(t *testing.T) {
function TestParseEntryWithMultipleAuthors (line 987) | func TestParseEntryWithMultipleAuthors(t *testing.T) {
function TestParseFeedWithEntryWithoutAuthor (line 1016) | func TestParseFeedWithEntryWithoutAuthor(t *testing.T) {
function TestParseFeedWithMultipleAuthors (line 1042) | func TestParseFeedWithMultipleAuthors(t *testing.T) {
function TestParseFeedWithoutAuthor (line 1074) | func TestParseFeedWithoutAuthor(t *testing.T) {
function TestParseEntryWithEnclosures (line 1097) | func TestParseEntryWithEnclosures(t *testing.T) {
function TestParseEntryWithRelativeEnclosureURL (line 1178) | func TestParseEntryWithRelativeEnclosureURL(t *testing.T) {
function TestParseEntryWithDuplicateEnclosureURL (line 1217) | func TestParseEntryWithDuplicateEnclosureURL(t *testing.T) {
function TestParseEntryWithoutEnclosureURL (line 1261) | func TestParseEntryWithoutEnclosureURL(t *testing.T) {
function TestParseEntryWithPublished (line 1298) | func TestParseEntryWithPublished(t *testing.T) {
function TestParseEntryWithPublishedAndUpdated (line 1323) | func TestParseEntryWithPublishedAndUpdated(t *testing.T) {
function TestParseInvalidXml (line 1349) | func TestParseInvalidXml(t *testing.T) {
function TestParseTitleWithSingleQuote (line 1357) | func TestParseTitleWithSingleQuote(t *testing.T) {
function TestParseTitleWithEncodedSingleQuote (line 1376) | func TestParseTitleWithEncodedSingleQuote(t *testing.T) {
function TestParseTitleWithSingleQuoteAndHTMLType (line 1395) | func TestParseTitleWithSingleQuoteAndHTMLType(t *testing.T) {
function TestParseWithHTMLEntity (line 1414) | func TestParseWithHTMLEntity(t *testing.T) {
function TestParseWithInvalidCharacterEntity (line 1433) | func TestParseWithInvalidCharacterEntity(t *testing.T) {
function TestParseMediaGroup (line 1452) | func TestParseMediaGroup(t *testing.T) {
function TestParseMediaElements (line 1519) | func TestParseMediaElements(t *testing.T) {
function TestParseRepliesLinkRelationWithHTMLType (line 1587) | func TestParseRepliesLinkRelationWithHTMLType(t *testing.T) {
function TestParseRepliesLinkRelationWithXHTMLType (line 1631) | func TestParseRepliesLinkRelationWithXHTMLType(t *testing.T) {
function TestParseRepliesLinkRelationWithNoType (line 1675) | func TestParseRepliesLinkRelationWithNoType(t *testing.T) {
function TestAbsoluteCommentsURL (line 1714) | func TestAbsoluteCommentsURL(t *testing.T) {
function TestParseItemWithCategories (line 1754) | func TestParseItemWithCategories(t *testing.T) {
function TestParseFeedWithCategories (line 1789) | func TestParseFeedWithCategories(t *testing.T) {
function TestParseFeedWithIconURL (line 1823) | func TestParseFeedWithIconURL(t *testing.T) {
FILE: internal/reader/atom/atom_common.go
type AtomPerson (line 11) | type AtomPerson struct
method PersonName (line 24) | func (a *AtomPerson) PersonName() string {
type atomPersons (line 33) | type atomPersons
method personNames (line 35) | func (a atomPersons) personNames() []string {
type AtomLink (line 51) | type AtomLink struct
type atomLinks (line 59) | type atomLinks
method originalLink (line 61) | func (a atomLinks) originalLink() string {
method firstLinkWithRelation (line 75) | func (a atomLinks) firstLinkWithRelation(relation string) string {
method firstLinkWithRelationAndType (line 85) | func (a atomLinks) firstLinkWithRelationAndType(relation string, conte...
method findAllLinksWithRelation (line 99) | func (a atomLinks) findAllLinksWithRelation(relation string) []*AtomLi...
type atomCategory (line 119) | type atomCategory struct
type atomCategories (line 137) | type atomCategories
method CategoryNames (line 139) | func (ac atomCategories) CategoryNames() []string {
FILE: internal/reader/atom/parser.go
function Parse (line 15) | func Parse(baseURL string, r io.ReadSeeker, version string) (*model.Feed...
FILE: internal/reader/date/parser.go
function Parse (line 318) | func Parse(rawInput string) (t time.Time, err error) {
function parseLocalTimeDates (line 350) | func parseLocalTimeDates(layout, ds string) (t time.Time, err error) {
function checkTimezoneRange (line 366) | func checkTimezoneRange(t time.Time) time.Time {
FILE: internal/reader/date/parser_test.go
function FuzzParse (line 10) | func FuzzParse(f *testing.F) {
function TestParseEmptyDate (line 18) | func TestParseEmptyDate(t *testing.T) {
function TestParseInvalidDate (line 24) | func TestParseInvalidDate(t *testing.T) {
function TestParseAtomDate (line 30) | func TestParseAtomDate(t *testing.T) {
function TestParseRSSDateTimezone (line 48) | func TestParseRSSDateTimezone(t *testing.T) {
function TestParseRSSDateGMT (line 77) | func TestParseRSSDateGMT(t *testing.T) {
function TestParseRSSDatePST (line 106) | func TestParseRSSDatePST(t *testing.T) {
function TestParseRSSDateEST (line 135) | func TestParseRSSDateEST(t *testing.T) {
function TestParseRSSDateOffset (line 163) | func TestParseRSSDateOffset(t *testing.T) {
function TestParseWeirdDateFormat (line 181) | func TestParseWeirdDateFormat(t *testing.T) {
function TestParseDateWithTimezoneOutOfRange (line 238) | func TestParseDateWithTimezoneOutOfRange(t *testing.T) {
FILE: internal/reader/dublincore/dublincore.go
type DublinCoreChannelElement (line 6) | type DublinCoreChannelElement struct
type DublinCoreItemElement (line 10) | type DublinCoreItemElement struct
FILE: internal/reader/encoding/encoding.go
function CharsetReader (line 26) | func CharsetReader(charsetLabel string, input io.Reader) (io.Reader, err...
function NewCharsetReader (line 45) | func NewCharsetReader(r io.Reader, contentType string) (io.Reader, error) {
function NewCharsetReaderFromBytes (line 54) | func NewCharsetReaderFromBytes(buffer []byte, contentType string) (io.Re...
FILE: internal/reader/encoding/encoding_test.go
function TestCharsetReaderWithUTF8 (line 16) | func TestCharsetReaderWithUTF8(t *testing.T) {
function TestCharsetReaderWithISO88591 (line 44) | func TestCharsetReaderWithISO88591(t *testing.T) {
function TestCharsetReaderWithWindows1252 (line 72) | func TestCharsetReaderWithWindows1252(t *testing.T) {
function TestCharsetReaderWithInvalidProlog (line 100) | func TestCharsetReaderWithInvalidProlog(t *testing.T) {
function TestCharsetReaderWithUTF8DocumentWithIncorrectProlog (line 128) | func TestCharsetReaderWithUTF8DocumentWithIncorrectProlog(t *testing.T) {
function TestCharsetReaderWithWindows1252DocumentWithIncorrectProlog (line 156) | func TestCharsetReaderWithWindows1252DocumentWithIncorrectProlog(t *test...
function TestNewReaderWithUTF8Document (line 184) | func TestNewReaderWithUTF8Document(t *testing.T) {
function TestNewReaderWithUTF8DocumentAndNoContentEncoding (line 212) | func TestNewReaderWithUTF8DocumentAndNoContentEncoding(t *testing.T) {
function TestNewReaderWithISO88591Document (line 240) | func TestNewReaderWithISO88591Document(t *testing.T) {
function TestNewReaderWithISO88591DocumentAndNoContentType (line 268) | func TestNewReaderWithISO88591DocumentAndNoContentType(t *testing.T) {
function TestNewReaderWithISO88591DocumentWithMetaAfter1024Bytes (line 296) | func TestNewReaderWithISO88591DocumentWithMetaAfter1024Bytes(t *testing....
function TestNewReaderWithUTF8DocumentWithMetaAfter1024Bytes (line 324) | func TestNewReaderWithUTF8DocumentWithMetaAfter1024Bytes(t *testing.T) {
function TestCharsetReaderWithKOI8RLabel (line 352) | func TestCharsetReaderWithKOI8RLabel(t *testing.T) {
function TestCharsetReaderWithUppercaseKOI8RLabel (line 379) | func TestCharsetReaderWithUppercaseKOI8RLabel(t *testing.T) {
function TestCharsetReaderWithKOI8RFeedFixture (line 406) | func TestCharsetReaderWithKOI8RFeedFixture(t *testing.T) {
function TestNewCharsetReaderWithKOI8RContentType (line 437) | func TestNewCharsetReaderWithKOI8RContentType(t *testing.T) {
function TestNewCharsetReaderWithKOI8RFeedFixtureAndContentType (line 464) | func TestNewCharsetReaderWithKOI8RFeedFixtureAndContentType(t *testing.T) {
FILE: internal/reader/fetcher/encoding_wrappers.go
type brotliReadCloser (line 13) | type brotliReadCloser struct
method Read (line 25) | func (b *brotliReadCloser) Read(p []byte) (n int, err error) {
method Close (line 29) | func (b *brotliReadCloser) Close() error {
function NewBrotliReadCloser (line 18) | func NewBrotliReadCloser(body io.ReadCloser) *brotliReadCloser {
type gzipReadCloser (line 33) | type gzipReadCloser struct
method Read (line 43) | func (gz *gzipReadCloser) Read(p []byte) (n int, err error) {
method Close (line 56) | func (gz *gzipReadCloser) Close() error {
function NewGzipReadCloser (line 39) | func NewGzipReadCloser(body io.ReadCloser) *gzipReadCloser {
FILE: internal/reader/fetcher/request_builder.go
constant defaultHTTPClientTimeout (line 25) | defaultHTTPClientTimeout = 20 * time.Second
constant defaultAcceptHeader (line 26) | defaultAcceptHeader = "application/xml,application/atom+xml,applica...
type RequestBuilder (line 34) | type RequestBuilder struct
method WithHeader (line 54) | func (r *RequestBuilder) WithHeader(key, value string) *RequestBuilder {
method WithETag (line 59) | func (r *RequestBuilder) WithETag(etag string) *RequestBuilder {
method WithLastModified (line 66) | func (r *RequestBuilder) WithLastModified(lastModified string) *Reques...
method WithUserAgent (line 73) | func (r *RequestBuilder) WithUserAgent(userAgent string, defaultUserAg...
method WithCookie (line 82) | func (r *RequestBuilder) WithCookie(cookie string) *RequestBuilder {
method WithUsernameAndPassword (line 89) | func (r *RequestBuilder) WithUsernameAndPassword(username, password st...
method WithProxyRotator (line 96) | func (r *RequestBuilder) WithProxyRotator(proxyRotator *proxyrotator.P...
method WithCustomApplicationProxyURL (line 101) | func (r *RequestBuilder) WithCustomApplicationProxyURL(proxyURL *url.U...
method UseCustomApplicationProxyURL (line 106) | func (r *RequestBuilder) UseCustomApplicationProxyURL(value bool) *Req...
method WithCustomFeedProxyURL (line 111) | func (r *RequestBuilder) WithCustomFeedProxyURL(proxyURL string) *Requ...
method WithTimeout (line 116) | func (r *RequestBuilder) WithTimeout(timeout time.Duration) *RequestBu...
method WithoutRedirects (line 121) | func (r *RequestBuilder) WithoutRedirects() *RequestBuilder {
method DisableHTTP2 (line 126) | func (r *RequestBuilder) DisableHTTP2(value bool) *RequestBuilder {
method IgnoreTLSErrors (line 131) | func (r *RequestBuilder) IgnoreTLSErrors(value bool) *RequestBuilder {
method WithoutCompression (line 136) | func (r *RequestBuilder) WithoutCompression() *RequestBuilder {
method ExecuteRequest (line 141) | func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Resp...
function NewRequestBuilder (line 47) | func NewRequestBuilder() *RequestBuilder {
FILE: internal/reader/fetcher/request_builder_test.go
function TestNewRequestBuilder (line 17) | func TestNewRequestBuilder(t *testing.T) {
function TestRequestBuilder_WithHeader (line 30) | func TestRequestBuilder_WithHeader(t *testing.T) {
function TestRequestBuilder_WithETag (line 47) | func TestRequestBuilder_WithETag(t *testing.T) {
function TestRequestBuilder_WithLastModified (line 77) | func TestRequestBuilder_WithLastModified(t *testing.T) {
function TestRequestBuilder_WithUserAgent (line 107) | func TestRequestBuilder_WithUserAgent(t *testing.T) {
function TestRequestBuilder_WithCookie (line 138) | func TestRequestBuilder_WithCookie(t *testing.T) {
function TestRequestBuilder_WithUsernameAndPassword (line 168) | func TestRequestBuilder_WithUsernameAndPassword(t *testing.T) {
function TestRequestBuilder_DefaultAcceptHeader (line 201) | func TestRequestBuilder_DefaultAcceptHeader(t *testing.T) {
function TestRequestBuilder_CustomAcceptHeaderNotOverridden (line 218) | func TestRequestBuilder_CustomAcceptHeaderNotOverridden(t *testing.T) {
function TestRequestBuilder_WithTimeout (line 236) | func TestRequestBuilder_WithTimeout(t *testing.T) {
function TestRequestBuilder_WithoutRedirects (line 245) | func TestRequestBuilder_WithoutRedirects(t *testing.T) {
function TestRequestBuilder_DisableHTTP2 (line 269) | func TestRequestBuilder_DisableHTTP2(t *testing.T) {
function TestRequestBuilder_IgnoreTLSErrors (line 278) | func TestRequestBuilder_IgnoreTLSErrors(t *testing.T) {
function TestRequestBuilder_WithoutCompression (line 287) | func TestRequestBuilder_WithoutCompression(t *testing.T) {
function TestRequestBuilder_WithCompression (line 304) | func TestRequestBuilder_WithCompression(t *testing.T) {
function TestRequestBuilder_ConnectionCloseHeader (line 321) | func TestRequestBuilder_ConnectionCloseHeader(t *testing.T) {
function TestRequestBuilder_WithCustomApplicationProxyURL (line 338) | func TestRequestBuilder_WithCustomApplicationProxyURL(t *testing.T) {
function TestRequestBuilder_UseCustomApplicationProxyURL (line 348) | func TestRequestBuilder_UseCustomApplicationProxyURL(t *testing.T) {
function TestRequestBuilder_WithCustomFeedProxyURL (line 357) | func TestRequestBuilder_WithCustomFeedProxyURL(t *testing.T) {
function TestRequestBuilder_ChainedMethods (line 367) | func TestRequestBuilder_ChainedMethods(t *testing.T) {
function TestRequestBuilder_InvalidURL (line 396) | func TestRequestBuilder_InvalidURL(t *testing.T) {
function TestRequestBuilder_RefusePrivateNetworkByDefault (line 404) | func TestRequestBuilder_RefusePrivateNetworkByDefault(t *testing.T) {
function TestRequestBuilder_AllowPrivateNetworkWhenEnabled (line 423) | func TestRequestBuilder_AllowPrivateNetworkWhenEnabled(t *testing.T) {
function TestRequestBuilder_RefusePrivateNetworkOnRedirect (line 439) | func TestRequestBuilder_RefusePrivateNetworkOnRedirect(t *testing.T) {
function TestRequestBuilder_TimeoutConfiguration (line 467) | func TestRequestBuilder_TimeoutConfiguration(t *testing.T) {
function configureFetcherAllowPrivateNetworksOption (line 490) | func configureFetcherAllowPrivateNetworksOption(t *testing.T, value stri...
FILE: internal/reader/fetcher/response_handler.go
type ResponseHandler (line 23) | type ResponseHandler struct
method EffectiveURL (line 32) | func (r *ResponseHandler) EffectiveURL() string {
method ContentType (line 36) | func (r *ResponseHandler) ContentType() string {
method LastModified (line 40) | func (r *ResponseHandler) LastModified() string {
method ETag (line 48) | func (r *ResponseHandler) ETag() string {
method Expires (line 56) | func (r *ResponseHandler) Expires() time.Duration {
method CacheControlMaxAge (line 68) | func (r *ResponseHandler) CacheControlMaxAge() time.Duration {
method ParseRetryDelay (line 82) | func (r *ResponseHandler) ParseRetryDelay() time.Duration {
method IsRateLimited (line 98) | func (r *ResponseHandler) IsRateLimited() bool {
method IsModified (line 102) | func (r *ResponseHandler) IsModified(lastEtagValue, lastModifiedValue ...
method IsRedirect (line 118) | func (r *ResponseHandler) IsRedirect() bool {
method Close (line 127) | func (r *ResponseHandler) Close() {
method getReader (line 133) | func (r *ResponseHandler) getReader(maxBodySize int64) io.ReadCloser {
method Body (line 152) | func (r *ResponseHandler) Body(maxBodySize int64) io.ReadCloser {
method ReadBody (line 156) | func (r *ResponseHandler) ReadBody(maxBodySize int64) ([]byte, *locale...
method LocalizedError (line 175) | func (r *ResponseHandler) LocalizedError() *locale.LocalizedErrorWrapp...
function NewResponseHandler (line 28) | func NewResponseHandler(httpResponse *http.Response, clientErr error) *R...
function isNetworkError (line 227) | func isNetworkError(err error) bool {
function isSSLError (line 241) | func isSSLError(err error) bool {
FILE: internal/reader/fetcher/response_handler_test.go
type testReadCloser (line 14) | type testReadCloser struct
method Read (line 18) | func (rc *testReadCloser) Read(_ []byte) (int, error) {
method Close (line 22) | func (rc *testReadCloser) Close() error {
function TestIsModified (line 27) | func TestIsModified(t *testing.T) {
function TestRetryDelay (line 87) | func TestRetryDelay(t *testing.T) {
function TestExpiresInMinutes (line 121) | func TestExpiresInMinutes(t *testing.T) {
function TestCacheControlMaxAgeInMinutes (line 155) | func TestCacheControlMaxAgeInMinutes(t *testing.T) {
function TestResponseHandlerCloseClosesBodyOnClientError (line 193) | func TestResponseHandlerCloseClosesBodyOnClientError(t *testing.T) {
FILE: internal/reader/filter/filter.go
type filterRule (line 37) | type filterRule struct
type filterRules (line 42) | type filterRules
function ParseRules (line 44) | func ParseRules(userRules, feedRules string) filterRules {
function parseRule (line 59) | func parseRule(userDefinedRule string) (bool, filterRule) {
function IsBlockedEntry (line 71) | func IsBlockedEntry(blockRules filterRules, allowRules filterRules, feed...
function matchesEntryRegexRules (line 101) | func matchesEntryRegexRules(regexPattern string, feed *model.Feed, entry...
function matchesEntryFilterRules (line 136) | func matchesEntryFilterRules(rules filterRules, feed *model.Feed, entry ...
function matchesRule (line 153) | func matchesRule(rule filterRule, entry *model.Entry) bool {
function isDateMatchingPattern (line 179) | func isDateMatchingPattern(pattern string, entryDate time.Time) bool {
function containsRegexPattern (line 229) | func containsRegexPattern(pattern string, items []string) bool {
function parseDuration (line 238) | func parseDuration(duration string) (time.Duration, error) {
FILE: internal/reader/filter/filter_test.go
function createTestEntry (line 14) | func createTestEntry() *model.Entry {
function createTestFeed (line 27) | func createTestFeed() *model.Feed {
function TestParseRules (line 39) | func TestParseRules(t *testing.T) {
function TestParseRule (line 101) | func TestParseRule(t *testing.T) {
function TestIsBlockedEntry (line 170) | func TestIsBlockedEntry(t *testing.T) {
function TestAllowRulesExclusiveBehavior (line 236) | func TestAllowRulesExclusiveBehavior(t *testing.T) {
function TestAllowRulesWithBlockRulesPrecedence (line 294) | func TestAllowRulesWithBlockRulesPrecedence(t *testing.T) {
function TestKeeplistRulesBehavior (line 345) | func TestKeeplistRulesBehavior(t *testing.T) {
function TestMatchesEntryRegexRules (line 413) | func TestMatchesEntryRegexRules(t *testing.T) {
function TestMatchesEntryFilterRules (line 503) | func TestMatchesEntryFilterRules(t *testing.T) {
function TestMatchesRule (line 552) | func TestMatchesRule(t *testing.T) {
function TestIsDateMatchingPattern (line 648) | func TestIsDateMatchingPattern(t *testing.T) {
function TestContainsRegexPattern (line 779) | func TestContainsRegexPattern(t *testing.T) {
function TestParseDuration (line 835) | func TestParseDuration(t *testing.T) {
function TestParseRulesEdgeCases (line 939) | func TestParseRulesEdgeCases(t *testing.T) {
function TestIsBlockedEntryWithRegexRules (line 982) | func TestIsBlockedEntryWithRegexRules(t *testing.T) {
function TestMatchesRuleWithInvalidRegex (line 1016) | func TestMatchesRuleWithInvalidRegex(t *testing.T) {
function TestIsDateMatchingPatternEdgeCases (line 1027) | func TestIsDateMatchingPatternEdgeCases(t *testing.T) {
function TestComplexFilterScenarios (line 1046) | func TestComplexFilterScenarios(t *testing.T) {
function TestFilterRulesWithSpecialCharacters (line 1089) | func TestFilterRulesWithSpecialCharacters(t *testing.T) {
function TestEntryWithEmptyFields (line 1150) | func TestEntryWithEmptyFields(t *testing.T) {
function TestBoundaryConditionsForDates (line 1203) | func TestBoundaryConditionsForDates(t *testing.T) {
function TestRegexErrorHandling (line 1255) | func TestRegexErrorHandling(t *testing.T) {
function TestParseDurationWithVariousFormats (line 1297) | func TestParseDurationWithVariousFormats(t *testing.T) {
function BenchmarkParseRules (line 1360) | func BenchmarkParseRules(b *testing.B) {
function BenchmarkIsBlockedEntry (line 1378) | func BenchmarkIsBlockedEntry(b *testing.B) {
function BenchmarkMatchesEntryRegexRules (line 1396) | func BenchmarkMatchesEntryRegexRules(b *testing.B) {
function BenchmarkIsDateMatchingPattern (line 1406) | func BenchmarkIsDateMatchingPattern(b *testing.B) {
function BenchmarkParseDuration (line 1415) | func BenchmarkParseDuration(b *testing.B) {
FILE: internal/reader/googleplay/googleplay.go
type GooglePlayChannelElement (line 9) | type GooglePlayChannelElement struct
type GooglePlayItemElement (line 17) | type GooglePlayItemElement struct
type GooglePlayImageElement (line 25) | type GooglePlayImageElement struct
type GooglePlayCategoryElement (line 29) | type GooglePlayCategoryElement struct
FILE: internal/reader/handler/handler.go
function getTranslatedLocalizedError (line 30) | func getTranslatedLocalizedError(store *storage.Storage, userID int64, o...
function CreateFeedFromSubscriptionDiscovery (line 40) | func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID ...
function CreateFeed (line 116) | func CreateFeed(store *storage.Storage, userID int64, feedCreationReques...
function RefreshFeed (line 207) | func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefr...
FILE: internal/reader/icon/checker.go
type iconChecker (line 16) | type iconChecker struct
method UpdateOrCreateFeedIcon (line 28) | func (c *iconChecker) UpdateOrCreateFeedIcon() {
method CreateFeedIconIfMissing (line 74) | func (c *iconChecker) CreateFeedIconIfMissing() {
function NewIconChecker (line 21) | func NewIconChecker(store *storage.Storage, feed *model.Feed) *iconCheck...
FILE: internal/reader/icon/finder.go
type iconFinder (line 35) | type iconFinder struct
method findIcon (line 49) | func (f *iconFinder) findIcon() (*model.Icon, error) {
method fetchDefaultIcon (line 87) | func (f *iconFinder) fetchDefaultIcon() (*model.Icon, error) {
method fetchIconsFromHTMLDocument (line 105) | func (f *iconFinder) fetchIconsFromHTMLDocument(documentURL string) (*...
method downloadIcon (line 157) | func (f *iconFinder) downloadIcon(iconURL string) (*model.Icon, error) {
function newIconFinder (line 41) | func newIconFinder(requestBuilder *fetcher.RequestBuilder, websiteURL, f...
function resizeIcon (line 186) | func resizeIcon(icon *model.Icon) *model.Icon {
function findIconURLsFromHTMLDocument (line 273) | func findIconURLsFromHTMLDocument(documentURL string, body io.Reader, co...
function parseImageDataURL (line 318) | func parseImageDataURL(value string) (*model.Icon, error) {
FILE: internal/reader/icon/finder_test.go
function TestParseImageDataURL (line 18) | func TestParseImageDataURL(t *testing.T) {
function TestParseImageDataURLWithNoEncoding (line 34) | func TestParseImageDataURLWithNoEncoding(t *testing.T) {
function TestParseImageWithRawSVGEncodedInUTF8 (line 54) | func TestParseImageWithRawSVGEncodedInUTF8(t *testing.T) {
function TestParseImageDataURLWithNoMediaTypeAndNoEncoding (line 74) | func TestParseImageDataURLWithNoMediaTypeAndNoEncoding(t *testing.T) {
function TestParseInvalidImageDataURLWithBadMimeType (line 82) | func TestParseInvalidImageDataURLWithBadMimeType(t *testing.T) {
function TestParseInvalidImageDataURLWithUnsupportedEncoding (line 89) | func TestParseInvalidImageDataURLWithUnsupportedEncoding(t *testing.T) {
function TestParseInvalidImageDataURLWithNoData (line 96) | func TestParseInvalidImageDataURLWithNoData(t *testing.T) {
function TestParseInvalidImageDataURL (line 103) | func TestParseInvalidImageDataURL(t *testing.T) {
function TestParseInvalidImageDataURLWithWrongPrefix (line 110) | func TestParseInvalidImageDataURLWithWrongPrefix(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_MultipleIcons (line 117) | func TestFindIconURLsFromHTMLDocument_MultipleIcons(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_CaseInsensitiveRel (line 151) | func TestFindIconURLsFromHTMLDocument_CaseInsensitiveRel(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_NoIcons (line 189) | func TestFindIconURLsFromHTMLDocument_NoIcons(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_EmptyHref (line 209) | func TestFindIconURLsFromHTMLDocument_EmptyHref(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_DataURLs (line 236) | func TestFindIconURLsFromHTMLDocument_DataURLs(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_RelativeAndAbsoluteURLs (line 268) | func TestFindIconURLsFromHTMLDocument_RelativeAndAbsoluteURLs(t *testing...
function TestFindIconURLsFromHTMLDocument_InvalidHTML (line 304) | func TestFindIconURLsFromHTMLDocument_InvalidHTML(t *testing.T) {
function TestFindIconURLsFromHTMLDocument_EmptyDocument (line 338) | func TestFindIconURLsFromHTMLDocument_EmptyDocument(t *testing.T) {
function TestResizeIconSmallGif (line 349) | func TestResizeIconSmallGif(t *testing.T) {
function TestResizeIconPng (line 363) | func TestResizeIconPng(t *testing.T) {
function TestResizeIconWebp (line 388) | func TestResizeIconWebp(t *testing.T) {
function TestResizeInvalidImage (line 403) | func TestResizeInvalidImage(t *testing.T) {
function TestResizeIconTooLargeDimensions (line 413) | func TestResizeIconTooLargeDimensions(t *testing.T) {
function TestResizeIconTooLargePixelCount (line 424) | func TestResizeIconTooLargePixelCount(t *testing.T) {
function TestMinifySvg (line 435) | func TestMinifySvg(t *testing.T) {
function TestMinifySvgWithError (line 445) | func TestMinifySvgWithError(t *testing.T) {
function mustMinimalPNG (line 469) | func mustMinimalPNG(t *testing.T, width, height uint32) []byte {
function writePNGChunk (line 485) | func writePNGChunk(t *testing.T, b *bytes.Buffer, chunkType string, fill...
FILE: internal/reader/itunes/itunes.go
type ItunesChannelElement (line 9) | type ItunesChannelElement struct
method GetItunesCategories (line 25) | func (i *ItunesChannelElement) GetItunesCategories() []string {
type ItunesItemElement (line 36) | type ItunesItemElement struct
type ItunesImageElement (line 50) | type ItunesImageElement struct
type ItunesCategoryElement (line 54) | type ItunesCategoryElement struct
type ItunesOwnerElement (line 59) | type ItunesOwnerElement struct
method String (line 64) | func (i *ItunesOwnerElement) String() string {
FILE: internal/reader/json/adapter.go
type JSONAdapter (line 19) | type JSONAdapter struct
method BuildFeed (line 27) | func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
function NewJSONAdapter (line 23) | func NewJSONAdapter(jsonFeed *JSONFeed) *JSONAdapter {
FILE: internal/reader/json/json.go
type JSONFeed (line 11) | type JSONFeed struct
type JSONAuthor (line 56) | type JSONAuthor struct
type JSONAuthors (line 69) | type JSONAuthors
method UnmarshalJSON (line 71) | func (a *JSONAuthors) UnmarshalJSON(data []byte) error {
type JSONHub (line 88) | type JSONHub struct
type JSONItem (line 96) | type JSONItem struct
type JSONAttachment (line 151) | type JSONAttachment struct
FILE: internal/reader/json/parser.go
function Parse (line 15) | func Parse(baseURL string, data io.Reader) (*model.Feed, error) {
FILE: internal/reader/json/parser_test.go
function TestParseJsonFeedVersion1 (line 14) | func TestParseJsonFeedVersion1(t *testing.T) {
function TestParseFeedWithDescription (line 98) | func TestParseFeedWithDescription(t *testing.T) {
function TestParsePodcast (line 118) | func TestParsePodcast(t *testing.T) {
function TestParseFeedWithFeedURLWithTrailingSpace (line 205) | func TestParseFeedWithFeedURLWithTrailingSpace(t *testing.T) {
function TestParseFeedWithRelativeFeedURL (line 224) | func TestParseFeedWithRelativeFeedURL(t *testing.T) {
function TestParseFeedSiteURLWithTrailingSpace (line 243) | func TestParseFeedSiteURLWithTrailingSpace(t *testing.T) {
function TestParseFeedWithRelativeSiteURL (line 262) | func TestParseFeedWithRelativeSiteURL(t *testing.T) {
function TestParseFeedWithoutTitle (line 281) | func TestParseFeedWithoutTitle(t *testing.T) {
function TestParseFeedWithoutHomePage (line 306) | func TestParseFeedWithoutHomePage(t *testing.T) {
function TestParseFeedWithoutFeedURL (line 331) | func TestParseFeedWithoutFeedURL(t *testing.T) {
function TestParseItemWithoutAttachmentURL (line 355) | func TestParseItemWithoutAttachmentURL(t *testing.T) {
function TestParseItemWithRelativeURL (line 394) | func TestParseItemWithRelativeURL(t *testing.T) {
function TestParseItemWithExternalURLAndNoURL (line 419) | func TestParseItemWithExternalURLAndNoURL(t *testing.T) {
function TestParseItemWithExternalURLAndURL (line 447) | func TestParseItemWithExternalURLAndURL(t *testing.T) {
function TestParseItemWithLegacyAuthorField (line 476) | func TestParseItemWithLegacyAuthorField(t *testing.T) {
function TestParseItemWithMultipleAuthorFields (line 512) | func TestParseItemWithMultipleAuthorFields(t *testing.T) {
function TestParseItemWithMultipleDuplicateAuthors (line 555) | func TestParseItemWithMultipleDuplicateAuthors(t *testing.T) {
function TestParseItemWithAuthorsObject (line 602) | func TestParseItemWithAuthorsObject(t *testing.T) {
function TestParseItemWithInvalidDate (line 635) | func TestParseItemWithInvalidDate(t *testing.T) {
function TestParseItemWithMissingTitleUsesSummaryFallback (line 666) | func TestParseItemWithMissingTitleUsesSummaryFallback(t *testing.T) {
function TestParseItemWithMissingTitleUsesContentTextFallback (line 695) | func TestParseItemWithMissingTitleUsesContentTextFallback(t *testing.T) {
function TestParseItemWithMissingTitleUsesHTMLFallback (line 724) | func TestParseItemWithMissingTitleUsesHTMLFallback(t *testing.T) {
function TestParseItemWithMissingTitleUsesURLFallback (line 753) | func TestParseItemWithMissingTitleUsesURLFallback(t *testing.T) {
function TestParseItemWithTooLongUnicodeTitle (line 780) | func TestParseItemWithTooLongUnicodeTitle(t *testing.T) {
function TestParseItemTitleWithXMLTags (line 811) | func TestParseItemTitleWithXMLTags(t *testing.T) {
function TestParseItemHashPrefersIDOverURL (line 838) | func TestParseItemHashPrefersIDOverURL(t *testing.T) {
function TestParseItemHashUsesURLWhenNoID (line 863) | func TestParseItemHashUsesURLWhenNoID(t *testing.T) {
function TestParseItemHashUsesExternalURLFallback (line 887) | func TestParseItemHashUsesExternalURLFallback(t *testing.T) {
function TestParseItemHashFallsBackToContent (line 915) | func TestParseItemHashFallsBackToContent(t *testing.T) {
function TestParseItemTags (line 941) | func TestParseItemTags(t *testing.T) {
function TestParseFeedFavicon (line 991) | func TestParseFeedFavicon(t *testing.T) {
function TestParseFeedIcon (line 1021) | func TestParseFeedIcon(t *testing.T) {
function TestParseFeedWithRelativeAttachmentURL (line 1051) | func TestParseFeedWithRelativeAttachmentURL(t *testing.T) {
function TestParseInvalidJSON (line 1087) | func TestParseInvalidJSON(t *testing.T) {
function TestParseNullJSONFeed (line 1095) | func TestParseNullJSONFeed(t *testing.T) {
FILE: internal/reader/media/media.go
type MediaItemElement (line 15) | type MediaItemElement struct
method AllMediaThumbnails (line 25) | func (e *MediaItemElement) AllMediaThumbnails() []Thumbnail {
method AllMediaContents (line 35) | func (e *MediaItemElement) AllMediaContents() []Content {
method AllMediaPeerLinks (line 45) | func (e *MediaItemElement) AllMediaPeerLinks() []PeerLink {
method FirstMediaDescription (line 55) | func (e *MediaItemElement) FirstMediaDescription() string {
type Group (line 72) | type Group struct
type Content (line 80) | type Content struct
method MimeType (line 88) | func (mc *Content) MimeType() string {
method Size (line 106) | func (mc *Content) Size() int64 {
type Thumbnail (line 112) | type Thumbnail struct
method MimeType (line 117) | func (t *Thumbnail) MimeType() string {
method Size (line 122) | func (t *Thumbnail) Size() int64 {
type PeerLink (line 127) | type PeerLink struct
method MimeType (line 133) | func (p *PeerLink) MimeType() string {
method Size (line 141) | func (p *PeerLink) Size() int64 {
type Description (line 146) | type Description struct
method HTML (line 152) | func (d *Description) HTML() string {
type DescriptionList (line 162) | type DescriptionList
method First (line 165) | func (dl DescriptionList) First() string {
type MediaCategoryList (line 175) | type MediaCategoryList
method Labels (line 177) | func (mcl MediaCategoryList) Labels() []string {
type MediaCategory (line 188) | type MediaCategory struct
FILE: internal/reader/media/media_test.go
function TestContentMimeType (line 8) | func TestContentMimeType(t *testing.T) {
function TestContentSize (line 33) | func TestContentSize(t *testing.T) {
function TestPeerLinkType (line 55) | func TestPeerLinkType(t *testing.T) {
function TestDescription (line 77) | func TestDescription(t *testing.T) {
function TestFirstDescription (line 103) | func TestFirstDescription(t *testing.T) {
FILE: internal/reader/opml/handler.go
type Handler (line 15) | type Handler struct
method Export (line 20) | func (h *Handler) Export(userID int64) (string, error) {
method Import (line 41) | func (h *Handler) Import(userID int64, data io.Reader) error {
function NewHandler (line 90) | func NewHandler(store *storage.Storage) *Handler {
FILE: internal/reader/opml/opml.go
type opmlDocument (line 12) | type opmlDocument struct
type opmlHeader (line 19) | type opmlHeader struct
type opmlOutline (line 25) | type opmlOutline struct
method MarshalXML (line 34) | func (o opmlOutline) MarshalXML(e *xml.Encoder, start xml.StartElement...
method IsSubscription (line 51) | func (o opmlOutline) IsSubscription() bool {
method GetTitle (line 55) | func (o opmlOutline) GetTitle() string {
method GetSiteURL (line 75) | func (o opmlOutline) GetSiteURL() string {
type opmlOutlineCollection (line 83) | type opmlOutlineCollection
method HasChildren (line 85) | func (o opmlOutlineCollection) HasChildren() bool {
FILE: internal/reader/opml/parser.go
function parse (line 15) | func parse(data io.Reader) ([]subcription, error) {
function getSubscriptionsFromOutlines (line 30) | func getSubscriptionsFromOutlines(outlines opmlOutlineCollection, catego...
FILE: internal/reader/opml/parser_test.go
method equals (line 12) | func (s subcription) equals(subscription subcription) bool {
function TestParseOpmlWithoutCategories (line 18) | func TestParseOpmlWithoutCategories(t *testing.T) {
function TestParseOpmlWithCategories (line 59) | func TestParseOpmlWithCategories(t *testing.T) {
function TestParseOpmlWithEmptyTitleAndEmptySiteURL (line 98) | func TestParseOpmlWithEmptyTitleAndEmptySiteURL(t *testing.T) {
function TestParseOpmlVersion1 (line 131) | func TestParseOpmlVersion1(t *testing.T) {
function TestParseOpmlVersion1WithoutOuterOutline (line 169) | func TestParseOpmlVersion1WithoutOuterOutline(t *testing.T) {
function TestParseOpmlVersion1WithSeveralNestedOutlines (line 203) | func TestParseOpmlVersion1WithSeveralNestedOutlines(t *testing.T) {
function TestParseOpmlWithInvalidCharacterEntity (line 245) | func TestParseOpmlWithInvalidCharacterEntity(t *testing.T) {
function TestParseInvalidXML (line 278) | func TestParseInvalidXML(t *testing.T) {
FILE: internal/reader/opml/serializer.go
function serialize (line 16) | func serialize(subscriptions []subcription) string {
function convertSubscriptionsToOPML (line 34) | func convertSubscriptionsToOPML(subscriptions []subcription) *opmlDocume...
function groupSubscriptionsByFeed (line 65) | func groupSubscriptionsByFeed(subscriptions []subcription) map[string][]...
FILE: internal/reader/opml/serializer_test.go
function TestSerialize (line 11) | func TestSerialize(t *testing.T) {
function TestNormalizedCategoriesOrder (line 41) | func TestNormalizedCategoriesOrder(t *testing.T) {
FILE: internal/reader/opml/subscription.go
type subcription (line 7) | type subcription struct
FILE: internal/reader/parser/format.go
constant FormatRDF (line 16) | FormatRDF = "rdf"
constant FormatRSS (line 17) | FormatRSS = "rss"
constant FormatAtom (line 18) | FormatAtom = "atom"
constant FormatJSON (line 19) | FormatJSON = "json"
constant FormatUnknown (line 20) | FormatUnknown = "unknown"
constant maxTokensToConsider (line 23) | maxTokensToConsider = uint(50)
function DetectFeedFormat (line 26) | func DetectFeedFormat(r io.ReadSeeker) (string, string) {
function detectJSONFormat (line 67) | func detectJSONFormat(r io.ReadSeeker) (bool, error) {
FILE: internal/reader/parser/format_test.go
function TestDetectRDF (line 11) | func TestDetectRDF(t *testing.T) {
function TestDetectRSS (line 20) | func TestDetectRSS(t *testing.T) {
function TestDetectAtom10 (line 29) | func TestDetectAtom10(t *testing.T) {
function TestDetectAtom03 (line 38) | func TestDetectAtom03(t *testing.T) {
function TestDetectAtomWithISOCharset (line 47) | func TestDetectAtomWithISOCharset(t *testing.T) {
function TestDetectJSON (line 56) | func TestDetectJSON(t *testing.T) {
function TestDetectUnknown (line 70) | func TestDetectUnknown(t *testing.T) {
function TestDetectJSONWithLargeLeadingWhitespace (line 81) | func TestDetectJSONWithLargeLeadingWhitespace(t *testing.T) {
function TestDetectJSONWithMixedWhitespace (line 94) | func TestDetectJSONWithMixedWhitespace(t *testing.T) {
function TestDetectOnlyWhitespace (line 107) | func TestDetectOnlyWhitespace(t *testing.T) {
function TestDetectJSONSmallerThanBuffer (line 116) | func TestDetectJSONSmallerThanBuffer(t *testing.T) {
function TestDetectJSONWithWhitespaceSmallerThanBuffer (line 125) | func TestDetectJSONWithWhitespaceSmallerThanBuffer(t *testing.T) {
FILE: internal/reader/parser/parser.go
function ParseFeed (line 20) | func ParseFeed(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
FILE: internal/reader/parser/parser_test.go
function BenchmarkParse (line 12) | func BenchmarkParse(b *testing.B) {
function FuzzParse (line 32) | func FuzzParse(f *testing.F) {
function TestParseAtom03Feed (line 88) | func TestParseAtom03Feed(t *testing.T) {
function TestParseAtom10Feed (line 116) | func TestParseAtom10Feed(t *testing.T) {
function TestParseAtomFeedWithRelativeURL (line 148) | func TestParseAtomFeedWithRelativeURL(t *testing.T) {
function TestParseRSS (line 184) | func TestParseRSS(t *testing.T) {
function TestParseRSSFeedWithRelativeURL (line 210) | func TestParseRSSFeedWithRelativeURL(t *testing.T) {
function TestParseRDF (line 248) | func TestParseRDF(t *testing.T) {
function TestParseRDFWithRelativeURL (line 277) | func TestParseRDFWithRelativeURL(t *testing.T) {
function TestParseJson (line 314) | func TestParseJson(t *testing.T) {
function TestParseJsonFeedWithRelativeURL (line 344) | func TestParseJsonFeedWithRelativeURL(t *testing.T) {
function TestParseUnknownFeed (line 381) | func TestParseUnknownFeed(t *testing.T) {
function TestParseEmptyFeed (line 400) | func TestParseEmptyFeed(t *testing.T) {
FILE: internal/reader/processor/bilibili.go
function shouldFetchBilibiliWatchTime (line 24) | func shouldFetchBilibiliWatchTime(entry *model.Entry) bool {
function extractBilibiliVideoID (line 31) | func extractBilibiliVideoID(websiteURL string) (string, string, error) {
function fetchBilibiliWatchTime (line 45) | func fetchBilibiliWatchTime(websiteURL string) (int, error) {
FILE: internal/reader/processor/nebula.go
function shouldFetchNebulaWatchTime (line 12) | func shouldFetchNebulaWatchTime(entry *model.Entry) bool {
function fetchNebulaWatchTime (line 20) | func fetchNebulaWatchTime(websiteURL string) (int, error) {
FILE: internal/reader/processor/odysee.go
function shouldFetchOdyseeWatchTime (line 12) | func shouldFetchOdyseeWatchTime(entry *model.Entry) bool {
function fetchOdyseeWatchTime (line 20) | func fetchOdyseeWatchTime(websiteURL string) (int, error) {
FILE: internal/reader/processor/processor.go
function ProcessFeedEntries (line 27) | func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, userID...
function ProcessEntryWebPage (line 180) | func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *mod...
FILE: internal/reader/processor/reading_time.go
function fetchWatchTime (line 21) | func fetchWatchTime(websiteURL, query string, isoDate bool) (int, error) {
function updateEntryReadingTime (line 61) | func updateEntryReadingTime(store *storage.Storage, feed *model.Feed, en...
FILE: internal/reader/processor/utils.go
function parseISO8601Duration (line 18) | func parseISO8601Duration(duration string) (time.Duration, error) {
function minifyContent (line 62) | func minifyContent(content string) string {
FILE: internal/reader/processor/utils_test.go
function TestISO8601DurationParsing (line 11) | func TestISO8601DurationParsing(t *testing.T) {
function TestISO8601DurationParsingErrors (line 50) | func TestISO8601DurationParsingErrors(t *testing.T) {
function TestMinifyEntryContentWithWhitespace (line 93) | func TestMinifyEntryContentWithWhitespace(t *testing.T) {
function TestMinifyContentWithDefaultAttributes (line 102) | func TestMinifyContentWithDefaultAttributes(t *testing.T) {
function TestMinifyContentWithComments (line 111) | func TestMinifyContentWithComments(t *testing.T) {
function TestMinifyContentWithSpecialComments (line 120) | func TestMinifyContentWithSpecialComments(t *testing.T) {
FILE: internal/reader/processor/youtube.go
function isYouTubeVideoURL (line 20) | func isYouTubeVideoURL(websiteURL string) bool {
function getVideoIDFromYouTubeURL (line 24) | func getVideoIDFromYouTubeURL(websiteURL string) string {
function shouldFetchYouTubeWatchTimeForSingleEntry (line 33) | func shouldFetchYouTubeWatchTimeForSingleEntry(entry *model.Entry) bool {
function shouldFetchYouTubeWatchTimeInBulk (line 37) | func shouldFetchYouTubeWatchTimeInBulk() bool {
function fetchYouTubeWatchTimeForSingleEntry (line 41) | func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) {
function fetchYouTubeWatchTimeInBulk (line 45) | func fetchYouTubeWatchTimeInBulk(entries []*model.Entry) {
function fetchYouTubeWatchTimeFromApiInBulk (line 80) | func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]t...
FILE: internal/reader/processor/youtube_test.go
function TestGetYouTubeVideoIDFromURL (line 10) | func TestGetYouTubeVideoIDFromURL(t *testing.T) {
function TestIsYouTubeVideoURL (line 27) | func TestIsYouTubeVideoURL(t *testing.T) {
FILE: internal/reader/rdf/adapter.go
type rdfAdapter (line 19) | type rdfAdapter struct
method buildFeed (line 23) | func (r *rdfAdapter) buildFeed(baseURL string) *model.Feed {
function stripTags (line 109) | func stripTags(value string) string {
FILE: internal/reader/rdf/parser.go
function Parse (line 15) | func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) {
FILE: internal/reader/rdf/parser_test.go
function TestParseRDFSample (line 13) | func TestParseRDFSample(t *testing.T) {
function TestParseRDFSampleWithDublinCore (line 123) | func TestParseRDFSampleWithDublinCore(t *testing.T) {
function TestParseRDFFeedWithEmptyTitle (line 234) | func TestParseRDFFeedWithEmptyTitle(t *testing.T) {
function TestParseRDFFeedWithEmptyLink (line 259) | func TestParseRDFFeedWithEmptyLink(t *testing.T) {
function TestParseRDFFeedWithRelativeLink (line 288) | func TestParseRDFFeedWithRelativeLink(t *testing.T) {
function TestParseRDFFeedSiteURLWithTrailingSpace (line 318) | func TestParseRDFFeedSiteURLWithTrailingSpace(t *testing.T) {
function TestParseItemWithoutLink (line 348) | func TestParseItemWithoutLink(t *testing.T) {
function TestParseItemRelativeURL (line 381) | func TestParseItemRelativeURL(t *testing.T) {
function TestParseFeedWithURLWrappedInSpaces (line 406) | func TestParseFeedWithURLWrappedInSpaces(t *testing.T) {
function TestParseRDFItemWitEmptyTitleElement (line 470) | func TestParseRDFItemWitEmptyTitleElement(t *testing.T) {
function TestParseRDFItemWithDublinCoreTitleElement (line 502) | func TestParseRDFItemWithDublinCoreTitleElement(t *testing.T) {
function TestParseRDFItemWithDuplicateTitleElement (line 535) | func TestParseRDFItemWithDuplicateTitleElement(t *testing.T) {
function TestParseItemWithEncodedHTMLTitle (line 569) | func TestParseItemWithEncodedHTMLTitle(t *testing.T) {
function TestParseRDFWithContentEncoded (line 594) | func TestParseRDFWithContentEncoded(t *testing.T) {
function TestParseRDFWithEncodedHTMLDescription (line 627) | func TestParseRDFWithEncodedHTMLDescription(t *testing.T) {
function TestParseItemWithoutDate (line 660) | func TestParseItemWithoutDate(t *testing.T) {
function TestParseItemWithDublicCoreDate (line 687) | func TestParseItemWithDublicCoreDate(t *testing.T) {
function TestParseItemWithInvalidDublicCoreDate (line 715) | func TestParseItemWithInvalidDublicCoreDate(t *testing.T) {
function TestParseItemWithEncodedHTMLInDCCreatorField (line 744) | func TestParseItemWithEncodedHTMLInDCCreatorField(t *testing.T) {
function TestParseItemWithOnlyFeedAuthor (line 772) | func TestParseItemWithOnlyFeedAuthor(t *testing.T) {
function TestParseInvalidXml (line 806) | func TestParseInvalidXml(t *testing.T) {
function TestParseFeedWithHTMLEntity (line 814) | func TestParseFeedWithHTMLEntity(t *testing.T) {
function TestParseFeedWithInvalidCharacterEntity (line 833) | func TestParseFeedWithInvalidCharacterEntity(t *testing.T) {
FILE: internal/reader/rdf/rdf.go
type rdf (line 13) | type rdf struct
type rdfChannel (line 19) | type rdfChannel struct
type rdfItem (line 26) | type rdfItem struct
FILE: internal/reader/readability/readability.go
constant defaultTagsToScore (line 18) | defaultTagsToScore = "section,h2,h3,h4,h5,h6,p,td,pre,div"
type candidate (line 29) | type candidate struct
method Node (line 34) | func (c *candidate) Node() *html.Node {
method String
Condensed preview — 619 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,881K chars).
[
{
"path": ".devcontainer/devcontainer.json",
"chars": 767,
"preview": "{\n \"name\": \"Miniflux\",\n \"dockerComposeFile\": \"docker-compose.yml\",\n \"service\": \"app\",\n \"workspaceFolder\": \"/workspac"
},
{
"path": ".devcontainer/docker-compose.yml",
"chars": 765,
"preview": "services:\n app:\n image: mcr.microsoft.com/devcontainers/go:1-trixie # https://www.debian.org/releases/trixie/index.e"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2912,
"preview": "name: \"Bug Report\"\ndescription: \"Report a bug or unexpected behavior\"\ntitle: \"[Bug]: \"\ntype: \"Bug\"\nlabels: [\"triage need"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/documentation.yml",
"chars": 2598,
"preview": "name: \"Documentation Issue\"\ndescription: \"Report issues or suggest improvements for the documentation\"\ntitle: \"[Docs]: \""
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 2231,
"preview": "name: \"Feature Request\"\ndescription: \"Suggest an idea or improvement for the project\"\ntitle: \"[Feature]: \"\ntype: \"Featur"
},
{
"path": ".github/ISSUE_TEMPLATE/feed_issue.yml",
"chars": 2725,
"preview": "name: \"Feed/Website Issue\"\ndescription: \"Report problems with a specific feed or website\"\ntitle: \"[Feed Issue]: \"\ntype: "
},
{
"path": ".github/ISSUE_TEMPLATE/proposal.yml",
"chars": 3214,
"preview": "name: \"Proposal / RFC\"\ndescription: \"Propose a significant change, or architectural decision\"\ntitle: \"[Proposal]: \"\ntype"
},
{
"path": ".github/dependabot.yml",
"chars": 648,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"daily\"\n\n - package"
},
{
"path": ".github/pull_request_template.md",
"chars": 416,
"preview": "Have you followed these guidelines?\n\n- [ ] I have tested my changes\n- [ ] There are no breaking changes\n- [ ] I have tho"
},
{
"path": ".github/workflows/build_binaries.yml",
"chars": 724,
"preview": "name: Build Binaries\npermissions:\n contents: read\non:\n workflow_dispatch:\n push:\n tags:\n - '[0-9]+.[0-9]+.[0-"
},
{
"path": ".github/workflows/codeberg_mirror.yml",
"chars": 728,
"preview": "name: Mirror to Codeberg\n\non:\n push:\n branches: [ main ]\n delete:\n workflow_dispatch:\n\njobs:\n mirror:\n if: git"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 1253,
"preview": "name: \"CodeQL\"\n\npermissions: read-all\n\non:\n push:\n branches: [ main ]\n paths:\n - '**.js'\n - '**.go'\n "
},
{
"path": ".github/workflows/debian_packages.yml",
"chars": 2566,
"preview": "name: Debian Packages\npermissions: read-all\non:\n workflow_dispatch:\n push:\n tags:\n - '[0-9]+.[0-9]+.[0-9]+'\n "
},
{
"path": ".github/workflows/docker.yml",
"chars": 3343,
"preview": "name: Docker\non:\n schedule:\n - cron: '0 1 * * *'\n push:\n tags:\n - '[0-9]+.[0-9]+.[0-9]+'\n pull_request:\n "
},
{
"path": ".github/workflows/linters.yml",
"chars": 1247,
"preview": "name: Linters\npermissions: read-all\n\non:\n pull_request:\n branches:\n - main\n workflow_dispatch:\n\njobs:\n jshint:\n"
},
{
"path": ".github/workflows/rpm_packages.yml",
"chars": 1663,
"preview": "name: RPM Packages\npermissions: read-all\non:\n workflow_dispatch:\n push:\n tags:\n - '[0-9]+.[0-9]+.[0-9]+'\n sch"
},
{
"path": ".github/workflows/scripts/commit-checker.py",
"chars": 3117,
"preview": "import subprocess\nimport re\nimport sys\nimport argparse\nfrom typing import Match\n\n# Conventional commit pattern (includin"
},
{
"path": ".github/workflows/tests.yml",
"chars": 1488,
"preview": "name: Tests\npermissions: read-all\n\non:\n pull_request:\n branches:\n - main\n workflow_dispatch:\n\njobs:\n unit-tests"
},
{
"path": ".gitignore",
"chars": 58,
"preview": "./*.sha256\n/miniflux\n.idea\n.vscode\n*.deb\n*.rpm\nminiflux-*\n"
},
{
"path": ".golangci.yml",
"chars": 435,
"preview": "version: \"2\"\nlinters:\n default: standard\n disable:\n - errcheck\n enable:\n - errname\n - gocritic\n - goheade"
},
{
"path": "CONTRIBUTING.md",
"chars": 5240,
"preview": "# Contributing to Miniflux\n\nThis document outlines how to contribute effectively to Miniflux.\n\n## Philosophy\n\nMiniflux f"
},
{
"path": "LICENSE",
"chars": 10174,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 4752,
"preview": "APP := miniflux\nDOCKER_IMAGE := miniflux/miniflux\nVERSION := $(shell git describe --tags --exact-"
},
{
"path": "Procfile",
"chars": 18,
"preview": "web: miniflux.app\n"
},
{
"path": "README.md",
"chars": 8280,
"preview": "Miniflux 2\n==========\n\nMiniflux is a minimalist and opinionated feed reader.\nIt's simple, fast, lightweight and super ea"
},
{
"path": "SECURITY.md",
"chars": 524,
"preview": "# Security Policy\n\n## Supported Versions\n\nOnly the latest stable version is supported.\n\n## Reporting a Vulnerability\n\nPr"
},
{
"path": "client/README.md",
"chars": 1113,
"preview": "Miniflux API Client\n===================\n\n[](https://pkg.go.d"
},
{
"path": "client/client.go",
"chars": 35832,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "client/client_test.go",
"chars": 37043,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "client/doc.go",
"chars": 721,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "client/model.go",
"chars": 14806,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "client/options.go",
"chars": 719,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "client/request.go",
"chars": 4226,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "contrib/README.md",
"chars": 226,
"preview": "The contrib directory contains various useful things contributed by the community.\n\nCommunity contributions are not offi"
},
{
"path": "contrib/ansible/inventories/group_vars/miniflux_vars.yml",
"chars": 262,
"preview": "---\n miniflux_linux_user: miniflux\n miniflux_db_user_name: miniflux_db_user\n miniflux_db_user_password: miniflux_db_u"
},
{
"path": "contrib/ansible/playbooks/playbook.yml",
"chars": 80,
"preview": "---\n- hosts: miniflux\n roles:\n - { role: mgrote.miniflux, tags: \"miniflux\" }"
},
{
"path": "contrib/ansible/roles/mgrote.miniflux/README.md",
"chars": 552,
"preview": "## mgrote.miniflux\n\n### Details\nInstalls and configures Miniflux v2 with ansible\n\n### Works on...\n- [x] Ubuntu (>=18.04)"
},
{
"path": "contrib/ansible/roles/mgrote.miniflux/defaults/main.yml",
"chars": 0,
"preview": ""
},
{
"path": "contrib/ansible/roles/mgrote.miniflux/handlers/main.yml",
"chars": 216,
"preview": "---\n - name: start_miniflux.service\n become: yes\n systemd:\n name: miniflux\n state: restarted\n enab"
},
{
"path": "contrib/ansible/roles/mgrote.miniflux/tasks/main.yml",
"chars": 920,
"preview": " - name: add Apt-key for miniflux-repo\n become: yes\n apt_key:\n url: https://apt.miniflux.app/KEY.gpg\n s"
},
{
"path": "contrib/ansible/roles/mgrote.miniflux/templates/miniflux.conf",
"chars": 474,
"preview": "# See https://docs.miniflux.app/\n\nLISTEN_ADDR=0.0.0.0:{{ miniflux_port }}\nDATABASE_URL=user={{ miniflux_db_user_name }} "
},
{
"path": "contrib/bruno/README.md",
"chars": 204,
"preview": "This folder contains Miniflux API collection for [Bruno](https://www.usebruno.com).\n\nBruno is a lightweight alternative "
},
{
"path": "contrib/bruno/miniflux/Bookmark an entry.bru",
"chars": 344,
"preview": "meta {\n name: Bookmark an entry\n type: http\n seq: 37\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/bookm"
},
{
"path": "contrib/bruno/miniflux/Create a feed.bru",
"chars": 280,
"preview": "meta {\n name: Create a feed\n type: http\n seq: 19\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/feeds\n body: json\n auth: b"
},
{
"path": "contrib/bruno/miniflux/Create a new category.bru",
"chars": 265,
"preview": "meta {\n name: Create a new category\n type: http\n seq: 10\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/categories\n body: j"
},
{
"path": "contrib/bruno/miniflux/Create a new user.bru",
"chars": 289,
"preview": "meta {\n name: Create a new user\n type: http\n seq: 5\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/users\n body: json\n auth"
},
{
"path": "contrib/bruno/miniflux/Delete a category.bru",
"chars": 323,
"preview": "meta {\n name: Delete a category\n type: http\n seq: 12\n}\n\ndelete {\n url: {{minifluxBaseURL}}/v1/categories/{{categoryI"
},
{
"path": "contrib/bruno/miniflux/Delete a feed.bru",
"chars": 314,
"preview": "meta {\n name: Delete a feed\n type: http\n seq: 26\n}\n\ndelete {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}\n body: n"
},
{
"path": "contrib/bruno/miniflux/Delete a user.bru",
"chars": 302,
"preview": "meta {\n name: Delete a user\n type: http\n seq: 7\n}\n\ndelete {\n url: {{minifluxBaseURL}}/v1/users/{{userID}}\n body: no"
},
{
"path": "contrib/bruno/miniflux/Discover feeds.bru",
"chars": 270,
"preview": "meta {\n name: Discover feeds\n type: http\n seq: 18\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/discover\n body: json\n aut"
},
{
"path": "contrib/bruno/miniflux/Fetch entry website content.bru",
"chars": 359,
"preview": "meta {\n name: Fetch entry website content\n type: http\n seq: 39\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/entries/{{entry"
},
{
"path": "contrib/bruno/miniflux/Flush history.bru",
"chars": 273,
"preview": "meta {\n name: Flush history\n type: http\n seq: 40\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/flush-history\n body: none\n "
},
{
"path": "contrib/bruno/miniflux/Get a single entry.bru",
"chars": 336,
"preview": "meta {\n name: Get a single entry\n type: http\n seq: 36\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/entries/{{entryID}}\n bo"
},
{
"path": "contrib/bruno/miniflux/Get a single feed entry.bru",
"chars": 371,
"preview": "meta {\n name: Get a single feed entry\n type: http\n seq: 33\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/en"
},
{
"path": "contrib/bruno/miniflux/Get a single feed.bru",
"chars": 329,
"preview": "meta {\n name: Get a single feed\n type: http\n seq: 24\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}\n body: "
},
{
"path": "contrib/bruno/miniflux/Get a single user by ID.bru",
"chars": 262,
"preview": "meta {\n name: Get a single user by ID\n type: http\n seq: 3\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/users/{{userID}}\n b"
},
{
"path": "contrib/bruno/miniflux/Get a single user by username.bru",
"chars": 276,
"preview": "meta {\n name: Get a single user by username\n type: http\n seq: 4\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/users/{{userna"
},
{
"path": "contrib/bruno/miniflux/Get all categories.bru",
"chars": 217,
"preview": "meta {\n name: Get all categories\n type: http\n seq: 9\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/categories\n body: none\n "
},
{
"path": "contrib/bruno/miniflux/Get all entries.bru",
"chars": 283,
"preview": "meta {\n name: Get all entries\n type: http\n seq: 34\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/entries\n body: none\n auth"
},
{
"path": "contrib/bruno/miniflux/Get all feeds.bru",
"chars": 279,
"preview": "meta {\n name: Get all feeds\n type: http\n seq: 20\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds\n body: none\n auth: ba"
},
{
"path": "contrib/bruno/miniflux/Get all users.bru",
"chars": 207,
"preview": "meta {\n name: Get all users\n type: http\n seq: 2\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/users\n body: none\n auth: bas"
},
{
"path": "contrib/bruno/miniflux/Get category entries.bru",
"chars": 331,
"preview": "meta {\n name: Get category entries\n type: http\n seq: 16\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/categories/{{categoryI"
},
{
"path": "contrib/bruno/miniflux/Get category entry.bru",
"chars": 354,
"preview": "meta {\n name: Get category entry\n type: http\n seq: 17\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/categories/{{categoryID}"
},
{
"path": "contrib/bruno/miniflux/Get category feeds.bru",
"chars": 327,
"preview": "meta {\n name: Get category feeds\n type: http\n seq: 14\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/categories/{{categoryID}"
},
{
"path": "contrib/bruno/miniflux/Get current user.bru",
"chars": 207,
"preview": "meta {\n name: Get current user\n type: http\n seq: 1\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/me\n body: none\n auth: bas"
},
{
"path": "contrib/bruno/miniflux/Get feed counters.bru",
"chars": 292,
"preview": "meta {\n name: Get feed counters\n type: http\n seq: 21\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds/counters\n body: no"
},
{
"path": "contrib/bruno/miniflux/Get feed entries.bru",
"chars": 336,
"preview": "meta {\n name: Get feed entries\n type: http\n seq: 32\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries\n "
},
{
"path": "contrib/bruno/miniflux/Get feed icon by feed ID.bru",
"chars": 327,
"preview": "meta {\n name: Get feed icon by feed ID\n type: http\n seq: 27\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/i"
},
{
"path": "contrib/bruno/miniflux/Get feed icon by icon ID.bru",
"chars": 322,
"preview": "meta {\n name: Get feed icon by icon ID\n type: http\n seq: 28\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/icons/{{iconID}}\n "
},
{
"path": "contrib/bruno/miniflux/Get version and build information.bru",
"chars": 230,
"preview": "meta {\n name: Get version and build information\n type: http\n seq: 42\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/version\n "
},
{
"path": "contrib/bruno/miniflux/Mark all category entries as read.bru",
"chars": 353,
"preview": "meta {\n name: Mark all category entries as read\n type: http\n seq: 13\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/categorie"
},
{
"path": "contrib/bruno/miniflux/Mark all user entries as read.bru",
"chars": 335,
"preview": "meta {\n name: Mark all user entries as read\n type: http\n seq: 8\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/users/{{userID"
},
{
"path": "contrib/bruno/miniflux/Mark feed as read.bru",
"chars": 332,
"preview": "meta {\n name: Mark feed as read\n type: http\n seq: 29\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/mark-all"
},
{
"path": "contrib/bruno/miniflux/OPML Export.bru",
"chars": 299,
"preview": "meta {\n name: OPML Export\n type: http\n seq: 30\n}\n\nget {\n url: {{minifluxBaseURL}}/v1/export\n body: none\n auth: bas"
},
{
"path": "contrib/bruno/miniflux/OPML Import.bru",
"chars": 672,
"preview": "meta {\n name: OPML Import\n type: http\n seq: 31\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/import\n body: xml\n auth: bas"
},
{
"path": "contrib/bruno/miniflux/Refresh a single feed.bru",
"chars": 341,
"preview": "meta {\n name: Refresh a single feed\n type: http\n seq: 23\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/refr"
},
{
"path": "contrib/bruno/miniflux/Refresh all feeds.bru",
"chars": 291,
"preview": "meta {\n name: Refresh all feeds\n type: http\n seq: 22\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/feeds/refresh\n body: non"
},
{
"path": "contrib/bruno/miniflux/Refresh category feeds.bru",
"chars": 333,
"preview": "meta {\n name: Refresh category feeds\n type: http\n seq: 15\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/categories/{{categor"
},
{
"path": "contrib/bruno/miniflux/Save an entry.bru",
"chars": 337,
"preview": "meta {\n name: Save an entry\n type: http\n seq: 38\n}\n\npost {\n url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/save\n b"
},
{
"path": "contrib/bruno/miniflux/Update a category.bru",
"chars": 320,
"preview": "meta {\n name: Update a category\n type: http\n seq: 11\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}"
},
{
"path": "contrib/bruno/miniflux/Update a feed.bru",
"chars": 311,
"preview": "meta {\n name: Update a feed\n type: http\n seq: 25\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}\n body: json"
},
{
"path": "contrib/bruno/miniflux/Update a user.bru",
"chars": 299,
"preview": "meta {\n name: Update a user\n type: http\n seq: 6\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/users/{{userID}}\n body: json\n"
},
{
"path": "contrib/bruno/miniflux/Update entries status.bru",
"chars": 293,
"preview": "meta {\n name: Update entries status\n type: http\n seq: 35\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/entries\n body: json\n"
},
{
"path": "contrib/bruno/miniflux/Update entry.bru",
"chars": 335,
"preview": "meta {\n name: Update entry\n type: http\n seq: 41\n}\n\nput {\n url: {{minifluxBaseURL}}/v1/entries/{{entryID}}\n body: js"
},
{
"path": "contrib/bruno/miniflux/bruno.json",
"chars": 66,
"preview": "{\n \"version\": \"1\",\n \"name\": \"Miniflux\",\n \"type\": \"collection\"\n}"
},
{
"path": "contrib/bruno/miniflux/environments/Local.bru",
"chars": 111,
"preview": "vars {\n minifluxBaseURL: http://127.0.0.1:8080\n minifluxUsername: admin\n}\nvars:secret [\n minifluxPassword\n]\n"
},
{
"path": "contrib/docker-compose/Caddyfile",
"chars": 49,
"preview": "miniflux.example.org\nreverse_proxy miniflux:8080\n"
},
{
"path": "contrib/docker-compose/README.md",
"chars": 294,
"preview": "Docker-Compose Examples\n=======================\n\nHere are few Docker Compose examples:\n\n- `basic.yml`: Basic example\n- `"
},
{
"path": "contrib/docker-compose/basic.yml",
"chars": 909,
"preview": "services:\n miniflux:\n image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}\n container_name: miniflux\n restart: a"
},
{
"path": "contrib/docker-compose/caddy.yml",
"chars": 1020,
"preview": "services:\n caddy:\n image: caddy:2\n container_name: caddy\n depends_on:\n - miniflux\n ports:\n - \"80:"
},
{
"path": "contrib/docker-compose/traefik.yml",
"chars": 1626,
"preview": "services:\n traefik:\n image: \"traefik:v2.3\"\n container_name: traefik\n command:\n - \"--providers.docker=true"
},
{
"path": "contrib/grafana/README.md",
"chars": 31,
"preview": "Grafana Dashboard for Miniflux\n"
},
{
"path": "contrib/grafana/dashboard.json",
"chars": 39745,
"preview": "{\n \"__inputs\": [\n {\n \"name\": \"DS_PROMETHEUS\",\n \"label\": \"prometheus\",\n \"description\": \"\",\n \"type"
},
{
"path": "contrib/sysvinit/README.md",
"chars": 130,
"preview": "\nSystem-V init for e.g. http://devuan.org\n\nAssumes an executable `/usr/local/bin/miniflux`.\n\nConfigure in `etc/default/m"
},
{
"path": "contrib/sysvinit/etc/default/miniflux",
"chars": 334,
"preview": "# sourced by /etc/init.d/miniflux\n# see cluster port in pg_lsclusters and ls -Al /var/run/postgresql/\nexport DATABASE_UR"
},
{
"path": "contrib/sysvinit/etc/init.d/miniflux",
"chars": 3224,
"preview": "#! /bin/sh\n\n### BEGIN INIT INFO\n# Provides: miniflux\n# Required-Start: $syslog $network\n# Required-Stop: "
},
{
"path": "contrib/thunder_client/README.md",
"chars": 297,
"preview": "Miniflux API Collection for Thunder Client VS Code Extension\n==========================================================="
},
{
"path": "contrib/thunder_client/collection.json",
"chars": 26061,
"preview": "{\n \"client\": \"Thunder Client\",\n \"collectionName\": \"Miniflux v2\",\n \"dateExported\": \"2023-07-31T01:53:38.743Z\",\n "
},
{
"path": "go.mod",
"chars": 1509,
"preview": "module miniflux.app/v2\n\n// +heroku goVersion go1.26\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.12.0\n\tgithub.com/andyb"
},
{
"path": "go.sum",
"chars": 14316,
"preview": "github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=\ngithub.com/PuerkitoBio/goquery v1"
},
{
"path": "internal/api/api.go",
"chars": 4567,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/api_integration_test.go",
"chars": 90316,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/api_key_handlers.go",
"chars": 1849,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/api_test.go",
"chars": 3629,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/category_handlers.go",
"chars": 4777,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/enclosure_handlers.go",
"chars": 2188,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/entry_handlers.go",
"chars": 15538,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/feed_handlers.go",
"chars": 6454,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/icon_handlers.go",
"chars": 1346,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/messages.go",
"chars": 1568,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/middleware.go",
"chars": 5386,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/opml_handlers.go",
"chars": 958,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/subscription_handlers.go",
"chars": 2536,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/user_handlers.go",
"chars": 5903,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/api/version_handler.go",
"chars": 618,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/ask_credentials.go",
"chars": 1192,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/cleanup_tasks.go",
"chars": 2487,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/cli.go",
"chars": 8364,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/create_admin.go",
"chars": 1437,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/daemon.go",
"chars": 2501,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/export_feeds.go",
"chars": 751,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/flush_sessions.go",
"chars": 412,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/health_check.go",
"chars": 922,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/info.go",
"chars": 549,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/logger.go",
"chars": 1082,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/refresh_feeds.go",
"chars": 2057,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/reset_password.go",
"chars": 953,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/cli/scheduler.go",
"chars": 1566,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/config.go",
"chars": 409,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/options.go",
"chars": 28186,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/options_parsing_test.go",
"chars": 54667,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/parser.go",
"chars": 7887,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/parser_test.go",
"chars": 11854,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/validators.go",
"chars": 1468,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/config/validators_test.go",
"chars": 9062,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/crypto/crypto.go",
"chars": 1547,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/database/database.go",
"chars": 1704,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/database/migrations.go",
"chars": 39885,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/database/postgresql.go",
"chars": 641,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/fever/README.md",
"chars": 8409,
"preview": "# Miniflux Fever API\n\nThis document describes the Fever-compatible API implemented by the `internal/fever` package in th"
},
{
"path": "internal/fever/handler.go",
"chars": 15327,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/fever/middleware.go",
"chars": 2277,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/fever/response.go",
"chars": 2820,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/README.md",
"chars": 15310,
"preview": "# Miniflux Google Reader API\n\nThis document describes the Google Reader compatible API implemented by the `internal/goog"
},
{
"path": "internal/googlereader/handler.go",
"chars": 36672,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/item.go",
"chars": 2358,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/item_test.go",
"chars": 2606,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/middleware.go",
"chars": 5727,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/parameters.go",
"chars": 1935,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/prefix_suffix.go",
"chars": 1416,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/request_modifier.go",
"chars": 2903,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/response.go",
"chars": 3926,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/googlereader/stream.go",
"chars": 3382,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/client/client.go",
"chars": 1857,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/client/client_test.go",
"chars": 3262,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/cookie/cookie.go",
"chars": 1139,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/client_ip.go",
"chars": 2134,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/client_ip_test.go",
"chars": 5419,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/context.go",
"chars": 5117,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/context_test.go",
"chars": 13448,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/cookie.go",
"chars": 441,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/cookie_test.go",
"chars": 841,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/params.go",
"chars": 3124,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/request/params_test.go",
"chars": 6585,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/builder.go",
"chars": 4602,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/builder_test.go",
"chars": 12062,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/html.go",
"chars": 5087,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/html_test.go",
"chars": 6375,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/json.go",
"chars": 5050,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/json_test.go",
"chars": 9285,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/response.go",
"chars": 1129,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/response_test.go",
"chars": 995,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/text.go",
"chars": 480,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/text_test.go",
"chars": 1089,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/xml.go",
"chars": 822,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/response/xml_test.go",
"chars": 1979,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/server/healthcheck.go",
"chars": 703,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/server/httpd.go",
"chars": 6469,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/server/metrics.go",
"chars": 2215,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/server/middleware.go",
"chars": 1344,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/http/server/routes.go",
"chars": 2240,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/apprise/apprise.go",
"chars": 2357,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/archiveorg/archiveorg.go",
"chars": 1323,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/betula/betula.go",
"chars": 1752,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/cubox/cubox.go",
"chars": 1826,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/discord/discord.go",
"chars": 2887,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/espial/espial.go",
"chars": 2316,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/instapaper/instapaper.go",
"chars": 1570,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/integration.go",
"chars": 21980,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/integration_test.go",
"chars": 1591,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/karakeep/karakeep.go",
"chars": 4791,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linkace/linkace.go",
"chars": 2684,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linkding/linkding.go",
"chars": 2436,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linktaco/linktaco.go",
"chars": 3777,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linktaco/linktaco_test.go",
"chars": 12816,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linkwarden/linkwarden.go",
"chars": 2604,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/linkwarden/linkwarden_test.go",
"chars": 10668,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/matrixbot/client.go",
"chars": 6703,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
},
{
"path": "internal/integration/matrixbot/matrixbot.go",
"chars": 1386,
"preview": "// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.\n// SPDX-License-Identifier: Apache-2.0\n\n"
}
]
// ... and 419 more files (download for full content)
About this extraction
This page contains the full source code of the miniflux/v2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 619 files (4.3 MB), approximately 1.2M tokens, and a symbol index with 3065 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.