Compare commits

...

444 Commits

Author SHA1 Message Date
Frédéric Guillot f68046cce0 docs(changelog): update release notes for version 2.2.8 2025-04-22 21:01:43 -07:00
Frédéric Guillot d33e305af9 fix(api): hide_globally categories field should be a boolean 2025-04-21 19:43:25 -07:00
Frédéric Guillot 764212f37c refactor(js): replace DomHelper methods with standalone functions 2025-04-17 18:15:08 -07:00
Tali Auster e02b65d4bc fix: deal with navigator.share exceptions
Navigator.share returns a promise that's executed in the background, but
unless we await it explicitly, we won't get the exceptions in the
try/catch block.
2025-04-17 17:07:38 -07:00
Tali Auster fe7ec25a09 chore: fix indentation 2025-04-17 17:07:38 -07:00
Tali Auster 2959a4d2bf fix: clarify share flow in UI
Prior to this commit, to share an entry, a user has to click on the
share link and then copy the URL they are redirected to. The danger is
that they may right-click and copy the share link without actually
clicking on it, and therefore share a link that, when authenticated,
shares the entry, rather than actually sharing the entry.

Here, we avoid this misinterpretation by making sharing into a POST
request and using a form rather than a link.
2025-04-17 17:07:38 -07:00
AiraNadih 6b70a7dc81 feat(api): add update_content query parameter to /entries/{entryID}/fetch-content endpoint 2025-04-17 12:41:58 -07:00
dependabot[bot] 495c6aacd9 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.27 to 1.14.28
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.27 to 1.14.28.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.27...v1.14.28)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-16 19:27:11 -07:00
Frédéric Guillot bc228d0afe ci: add documentation issue template 2025-04-12 14:29:52 -07:00
Frédéric Guillot d139d8a6ce feat(cli): add -reset-feed-next-check-at argument 2025-04-11 15:56:57 -07:00
dependabot[bot] 28d0185e79 build(deps): bump github.com/PuerkitoBio/goquery from 1.10.2 to 1.10.3
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-version: 1.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-11 15:44:47 -07:00
Frédéric Guillot c87c93d85f feat(config): add SCHEDULER_ROUND_ROBIN_MAX_INTERVAL option
Add option to cap maximum refresh interval when RSS TTL, Retry-After, Cache-Control, or Expires headers specify excessively high values.
2025-04-11 15:40:32 -07:00
dependabot[bot] 0ef21e85c2 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 20:53:15 -07:00
dependabot[bot] 7438d67061 build(deps): bump github.com/tdewolff/minify/v2 from 2.22.4 to 2.23.1
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.4 to 2.23.1.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.4...v2.23.1)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:49:12 -07:00
dependabot[bot] e4399b4f9a build(deps): bump golang.org/x/image from 0.25.0 to 0.26.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/image/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:48:54 -07:00
dependabot[bot] b4f891705a build(deps): bump golang.org/x/oauth2 from 0.28.0 to 0.29.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:10:31 -07:00
dependabot[bot] 3d72404ace build(deps): bump golang.org/x/net from 0.38.0 to 0.39.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/net/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 19:55:08 -07:00
Matthaiks c4e92e6111 feat(locale): update Polish translation 2025-04-07 12:06:44 -07:00
Frédéric Guillot ef22e95f8b feat: implement proxy URL per feed 2025-04-06 21:05:19 -07:00
tssujt 7b344de846 feat(telegrambot): replace "Go to website" button with "Go to Miniflux" 2025-04-06 18:30:42 -07:00
Frédéric Guillot c45b51d1f8 feat: use Cache-Control max-age and Expires headers to calculate next check 2025-04-06 16:24:00 -07:00
Frédéric Guillot 0af1a6e121 refactor: avoid logging twice the feed errors in the background worker 2025-04-06 15:39:40 -07:00
Frédéric Guillot 535fd050b7 feat: add proxy rotation functionality 2025-04-06 14:59:00 -07:00
Frédéric Guillot d20e8a4e2c ci(linter): replace commitlint with a Python script 2025-04-05 20:41:34 -07:00
Qeynos 7514e8a0c1 feat(locale): update Chinese translation 2025-04-04 19:40:44 -07:00
Cthulhux ca3ede3183 Update de_DE.json
More translations
2025-04-04 19:37:53 -07:00
dependabot[bot] 038d33600a build(deps): bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.13.0 to 3.14.1.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.13.0...v3.14.1)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 17:34:17 -07:00
dependabot[bot] 1a3a174688 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.25 to 1.14.27
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.25 to 1.14.27.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.25...v1.14.27)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 19:25:34 -07:00
dependabot[bot] 6848c759b5 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.24 to 1.14.25
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.24 to 1.14.25.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.24...v1.14.25)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:53 -07:00
dependabot[bot] 10578f512f build(deps): bump github.com/go-webauthn/webauthn from 0.12.2 to 0.12.3
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.2...v0.12.3)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:40 -07:00
Frédéric Guillot f99dff5238 docs(changelog): update release notes for version 2.2.7 2025-04-01 17:18:32 -07:00
Frédéric Guillot 1936ced1ed docs: update README 2025-03-30 11:18:16 -07:00
Frédéric Guillot cb695e653a fix(security): use a more restrictive CSP for untrusted content 2025-03-29 19:49:41 -07:00
Matthaiks f57949c9a2 feat(locale): update Polish translation 2025-03-29 15:15:21 -07:00
Frédéric Guillot 94fb3d6159 fix: change labels from Read / Unread to "Mark as Read" 2025-03-28 17:34:44 -07:00
Frédéric Guillot 51560f191f fix(subscription): add /rss/feed.xml to the list of known feed URLs 2025-03-28 16:59:06 -07:00
Frédéric Guillot c0fcdf3b6e fix(ui): update entry tags display logic to show links based on user authentication 2025-03-28 15:53:08 -07:00
Frédéric Guillot 969efd2af7 fix(ui): update share feature to correctly select title element and handle empty title 2025-03-28 15:36:43 -07:00
Frédéric Guillot 87fccfee3a fix(ui): remove touch-action style because it could cause horizontal scrolling issue 2025-03-28 15:20:31 -07:00
Frédéric Guillot e643effefa feat: combine feed icon handlers to use only externalIconID 2025-03-28 14:29:19 -07:00
Frédéric Guillot c531be8780 fix: update Content-Security-Policy to use 'sandbox' directive 2025-03-28 13:06:59 -07:00
Frédéric Guillot 6e3cecc57e feat(locale): add Romanian translation
This contribution was sent outside GitHub. Credit to Laurentiu.
2025-03-28 11:11:20 -07:00
Tali Auster b3d385861f fix: log warning on an empty client secret
Because Miniflux runs as a confidential service, a missing client secret
is a mistake in configuration. An empty client secret appears to be
valid per RFC 6749 (and is in fact the default set by Miniflux!), so we
log a warning.
2025-03-27 20:00:09 -07:00
dependabot[bot] cc5a8fa4b3 build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-27 17:26:54 -07:00
Frédéric Guillot 78ff727064 fix: name "external_id": converting NULL to string is unsupported
Regresion introduced by commit df8bc742fb.
2025-03-26 20:51:57 -07:00
dependabot[bot] 54dad707a4 build(deps): bump github.com/tdewolff/minify/v2 from 2.22.3 to 2.22.4
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.3 to 2.22.4.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.3...v2.22.4)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-26 19:48:23 -07:00
Josiah Campbell df8bc742fb feat(googlereader): add feed icon field, endpoint
Adds an endpoint to the Google Reader integration to serve
feed icon URLs.
2025-03-25 20:55:16 -07:00
Frédéric Guillot e342a4f143 fix: address minor issues detected by Go linters 2025-03-24 20:48:46 -07:00
dependabot[bot] febb7b1748 build(deps): bump golangci/golangci-lint-action from 6 to 7
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 7.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:02:03 -07:00
Frédéric Guillot 537867a64b ci: replace GitHub Issue Markdown templates with YAML forms 2025-03-24 17:27:31 -07:00
Frédéric Guillot 82a6fe64ae feat(storage): reduce the number of SQL queries when fetching entry enclosures 2025-03-23 14:13:33 -07:00
dependabot[bot] 984a66b590 build(deps): bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 15:33:19 -07:00
dependabot[bot] bd79ad2a7d build(deps): bump github.com/tdewolff/minify/v2 from 2.22.2 to 2.22.3
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.2 to 2.22.3.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.2...v2.22.3)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 20:26:38 -07:00
dependabot[bot] b583de88f3 build(deps): bump github.com/tdewolff/minify/v2 from 2.21.3 to 2.22.2
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.21.3 to 2.22.2.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.21.3...v2.22.2)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 17:57:04 -07:00
dependabot[bot] baccb9e719 build(deps): bump github.com/coreos/go-oidc/v3 from 3.12.0 to 3.13.0
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.12.0 to 3.13.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.12.0...v3.13.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 19:30:26 -07:00
dependabot[bot] 0fd11693f3 build(deps): bump github.com/go-webauthn/webauthn from 0.12.1 to 0.12.2 (#3209)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.1 to 0.12.2.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.1...v0.12.2)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 18:02:27 -07:00
Matthaiks 05f7a34d43 feat(locale): update Polish translation 2025-03-07 11:36:25 -08:00
Frédéric Guillot 315e72c412 fix(rewrite): remove obsolete rule for webtoons.com 2025-03-06 20:11:03 -08:00
jvoisin f916373f55 fix: allow the <b> tag 2025-03-06 19:27:30 -08:00
jvoisin 5353211206 fix: allow the <u> tag in feeds 2025-03-06 19:26:26 -08:00
dependabot[bot] 999ab0df2b build(deps): bump golang.org/x/net from 0.36.0 to 0.37.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:29:56 -08:00
dependabot[bot] f8700c84b8 build(deps): bump golang.org/x/image from 0.24.0 to 0.25.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/image/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:22:29 -08:00
dependabot[bot] ddd916161e build(deps): bump golang.org/x/crypto from 0.35.0 to 0.36.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:16:27 -08:00
dependabot[bot] c3cd5e3109 build(deps): bump golang.org/x/oauth2 from 0.27.0 to 0.28.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:11:59 -08:00
dependabot[bot] 051441d875 build(deps): bump golang.org/x/term from 0.29.0 to 0.30.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/term/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 15:53:41 -08:00
Paul Stenius 0cf5051cf8 feat: show size of DB on the about page 2025-03-05 15:49:32 -08:00
Paul Stenius 12ca7aa085 feat: add a make add string command to add new localized strings 2025-03-04 21:22:33 -08:00
dependabot[bot] d53dc4cf28 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.21.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 16:47:52 -08:00
dependabot[bot] e7298b255a build(deps): bump golang.org/x/net from 0.35.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 16:45:39 -08:00
AiraNadih ad02f21d04 refactor(rewrite): reorganize referer rules and remove obsolete mapping 2025-03-02 19:40:52 -08:00
Frédéric Guillot 46323b3fe7 test(api): update base url after upgrading hugo 2025-03-02 19:32:09 -08:00
David Pedersen 86b3593fec docs: update client README to not reference deprecated function 2025-02-27 17:52:08 -08:00
Frédéric Guillot d7c504f48e fix(ui): avoid 500 error and NaN when marking as read a deleted entry
Steps to reproduce:

1. In /unread, open a feed's settings in a new tab. The feed must have unread entries in /unread.
2. In the new tab/window, delete the feed.
3. Without refreshing, mark an entry from the now-deleted feed as read.
4. Result: The total unread count in the UI header switches to NaN.
2025-02-26 16:39:38 -08:00
Matthaiks 6ad2001c7d feat(locale): update Polish translation 2025-02-26 16:33:18 -08:00
dependabot[bot] c5d4c762d8 build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:59:46 -08:00
dependabot[bot] 8b66c2be1d build(deps): bump golang.org/x/oauth2 from 0.26.0 to 0.27.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:58:57 -08:00
dependabot[bot] cfb22d7ec1 build(deps): bump github.com/go-webauthn/webauthn from 0.11.2 to 0.12.1
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.11.2 to 0.12.1.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.11.2...v0.12.1)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:54:07 -08:00
Maytham Alsudany f01ff067a5 fix(processor): add missing quotation marks to import comments 2025-02-24 16:34:26 -08:00
Uziskull 55a3f9fcc9 feat(integrations/ntfy): make ntfy topics configurable per feed, with default one as fallback 2025-02-24 16:29:08 -08:00
dependabot[bot] 996f6f68d2 build(deps): bump github.com/go-jose/go-jose/v4 from 4.0.2 to 4.0.5
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.2 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:03:48 -08:00
Matthaiks 3b8ac8b16a feat(locale): update Polish translation 2025-02-23 11:46:28 -08:00
Frédéric Guillot 381f03b56a fix(greader): return enclosures in streamItemContentsHandler response 2025-02-22 20:11:41 -08:00
jvoisin 117d711d7d feat(urlcleaner): add more Google Analytics parameters 2025-02-22 17:07:59 -08:00
dependabot[bot] 2f71804ff4 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-22 17:06:47 -08:00
Frédéric Guillot 600f19cc87 docs(changelog): update release notes for version 2.2.6 2025-02-22 16:25:31 -08:00
jvoisin 4a77e937af perf(sanitizer): remove two useless calls to strings.ReplaceAll
The [strings.Fields](https://pkg.go.dev/strings#Fields) considers `'\t', '\n',
'\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP).` as spaces, so no need to
remove them beforehand.

This is a continuation of f2f60a8f73
2025-02-18 19:42:39 -08:00
dependabot[bot] f7e30822c3 build(deps): bump golang in /packaging/debian
Bumps golang from 1.23-bookworm to 1.24-bookworm.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 19:39:27 -08:00
Frédéric Guillot 1cffb70362 ci: trigger packaging tests on pull requests 2025-02-17 20:48:42 -08:00
Frédéric Guillot 462ba8d7f7 feat(sanitizer): allow img tags with only a srcset and no src attribute 2025-02-15 18:03:36 -08:00
Frédéric Guillot 991c54150d fix(css): avoid aside overflow on the pagination menu 2025-02-15 17:15:52 -08:00
Frédéric Guillot 6eedf4111f fix(scraper): avoid encoding issue if charset meta tag is after 1024 bytes 2025-02-15 17:05:14 -08:00
Frédéric Guillot af1f966250 test(encoding): add unit tests for CharsetReader function 2025-02-15 15:40:07 -08:00
Frédéric Guillot 7f54b27079 fix(rss): handle item title with CDATA content correctly
Fix regression introduced in commit a3ce03cc
2025-02-15 14:51:27 -08:00
Frédéric Guillot a3ce03cc9d feat(rss): add workaround for RSS item title with HTML content 2025-02-14 21:21:49 -08:00
Frédéric Guillot 502db64b28 ci: update GitHub Actions workflows to use Go 1.24 2025-02-13 21:29:58 -08:00
Frédéric Guillot 033806df4b refactor(locale): sort JSON documents by key 2025-02-13 21:25:26 -08:00
Frédéric Guillot 50bd6eb5d9 feat(locale): update French translation 2025-02-13 21:15:29 -08:00
Sergio Moura 3387201634 feat(pushover): add integration with pushover.net 2025-02-13 20:50:37 -08:00
dependabot[bot] 76acd81fa4 build(deps): bump github.com/PuerkitoBio/goquery from 1.10.1 to 1.10.2
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 17:30:22 -08:00
krvpb024 28b235595e feat(locale): add Taiwanese POJ (nan-Latn-pehoeji) support 2025-02-13 17:29:13 -08:00
dependabot[bot] 9398f7abd7 build(deps): bump golang.org/x/net from 0.34.0 to 0.35.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/net/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 18:01:37 -08:00
dependabot[bot] 0f55c81e2b build(deps): bump golang.org/x/crypto from 0.32.0 to 0.33.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 16:21:33 -08:00
Frédéric Guillot f2f60a8f73 feat(sanitizer): improve text truncation with better space handling 2025-02-06 21:21:49 -08:00
Frédéric Guillot e777f12490 fix(sanitizer): correct HTML tag name from tfooter to tfoot 2025-02-06 21:16:29 -08:00
Frédéric Guillot 7bdf133b9d ci: add commitlint to validate PR commit messages 2025-02-06 20:11:56 -08:00
Julien Voisin 7eb1d15315 refactor(date): use an else-if instead of two if statements 2025-02-06 19:44:12 -08:00
Cthulhux a74186c555 fix(locale): missing hyphen in de_DE.json 2025-02-05 20:13:08 -08:00
dependabot[bot] fb2fdf17b7 build(deps): bump golang.org/x/image from 0.23.0 to 0.24.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/image/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 19:08:24 -08:00
dependabot[bot] 6edb3da977 build(deps): bump golang.org/x/term from 0.28.0 to 0.29.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/term/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 17:43:43 -08:00
dependabot[bot] c0309b6ad1 build(deps): bump golang.org/x/oauth2 from 0.25.0 to 0.26.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 17:33:18 -08:00
MDeLuise 7bf1dd6e7a fix: update Linkace integration to support API v2 2025-02-02 12:40:02 -08:00
Frédéric Guillot 6a008eee14 feat(ui): open the <details> tag in edit feed page when the feature is enabled 2025-01-31 17:21:49 -08:00
Matthaiks b9b2d6822a feat(locale): update Polish translation 2025-01-31 17:18:48 -08:00
Wesley van Tilburg 459284ab96 feat(integration): add webhook URL per feed 2025-01-31 16:33:11 -08:00
Julien Voisin b193bc212a refactor(xml): improve the performances of NewXMLDecoder
- Invert a condition to make the code more readable
- Extract the encoding directly from the slice of bytes instead of converting
  it to string first.
2025-01-30 19:37:06 -08:00
Frédéric Guillot 3ebeb38ade fix(api): return 500 response when JSON serialization fails 2025-01-30 18:19:50 -08:00
Frédéric Guillot c951ac2876 fix: json encoding is failing with dates at OAD and negative timezone offset 2025-01-30 18:19:50 -08:00
Julien Voisin 7275bc808a feat(urlcleaner): add trackers to the blocklist 2025-01-29 19:32:19 -08:00
Sangeeth Sudheer e40446ad3c fix(ui): Redirect correctly post feed removal from category feeds list
Currently, removing a feed from `/category/{id}/feeds` redirects incorrectly to `/feeds`. This change fixes it so that
removing a feed will now correctly redirect to `/category/{id}/feeds`. Removing a feed from `/feeds` is unaffected and
will work as it does currently.

To fix this, a new UI endpoint `/category/{categoryID}/feed/{feedID}/remove` is added and a corresponding handler method
to validate and perform the removal from DB.
2025-01-27 17:43:54 -08:00
CaptainArk fba23cf464 feat(integration): add Slack integration 2025-01-27 17:37:22 -08:00
Mikael Berthe bae872e79b feat(locale): update French translations
Fix some French translations.
2025-01-26 10:38:31 -08:00
Peter Dave Hello d0c9ef5acf chore(i18n): update and improve zh_TW Traditional Chinese translations 2025-01-25 14:56:14 -08:00
Frédéric Guillot 0c74497ef7 fix(js): regression introduced in commit ffe1be5
The default argument should be set to false.
2025-01-24 17:52:58 -08:00
Frédéric Guillot 369054b02d feat(processor): fetch YouTube watch time in bulk using the API 2025-01-24 15:16:23 -08:00
Frédéric Guillot c3c42b0c37 fix(scraper): update TechCrunch scraper rule 2025-01-23 19:29:32 -08:00
jvoisin 4938a94968 Remove another superfluous cast 2025-01-23 19:20:13 -08:00
jvoisin ffe1be59ea Use a default parameter for goToPage as isn't ~always called with a single one 2025-01-23 19:20:13 -08:00
jvoisin 2318e9011d Use proper types in app.js 2025-01-23 19:20:13 -08:00
jvoisin 67df305ac2 Use shortcuts to declare padding 2025-01-23 19:20:13 -08:00
jvoisin 2e57e3351b Remove superfluous parenthesis 2025-01-23 19:20:13 -08:00
jvoisin a412cde3b3 Don't define receivers on both values and pointer
And use `o` instead of `outline` as done everywhere else.
2025-01-23 19:20:13 -08:00
jvoisin abfd9306a4 Guard against a potential null dereference 2025-01-23 19:20:13 -08:00
jvoisin 8889e44b5d Simplify a condition 2025-01-23 19:20:13 -08:00
jvoisin 9657bf1f5b Don't define methods both on instance and pointer
See https://go.dev/tour/methods/8
2025-01-23 19:20:13 -08:00
jvoisin 8c5f88ac62 Remove superfluous parenthesis 2025-01-23 19:20:13 -08:00
jvoisin 736f8b4dac Don't use defer in a loop
As the body of request isn't used, we can sloe it immediately.
2025-01-23 19:20:13 -08:00
jvoisin 60e1d9e361 Broaden an error condition
`http.ErrNoCookie` isn't the only possible error value.
2025-01-23 19:20:13 -08:00
jvoisin 7967ce4df2 Remove a useless cast 2025-01-23 19:20:13 -08:00
jvoisin 71c7845c42 Anchor = removal in webauthn_handler.js
Since we're base64-encoding, `=` can only happen at the end, so no need to
traverse the whole payload.
2025-01-23 19:20:13 -08:00
jvoisin aa56d23551 Replace the deprecated window.pageYOffset with window.scollY
See https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY
2025-01-23 19:20:13 -08:00
jvoisin 3cd448099d Remove a useless return 2025-01-23 19:20:13 -08:00
Frédéric Guillot 47ccefba4e fix(css): --entry-content-aside-border-color is missing from
`system.css`
2025-01-22 21:00:50 -08:00
Frédéric Guillot 1faccc7eca fix(sanitizer): non-allowed attributes are not properly stripped
Regression introduced in commit 58178d90cb
2025-01-22 20:50:38 -08:00
Frédéric Guillot e74d875d95 feat(css): improve aside element position on smartphone 2025-01-22 20:24:33 -08:00
Frédéric Guillot 33063a7775 docs(changelog): update release notes for version 2.2.5 2025-01-20 11:12:46 -08:00
Frédéric Guillot 49c62db2e1 fix: update Wallabag URL label to avoid confusion 2025-01-18 17:22:49 -08:00
Frédéric Guillot 400e8974f9 fix: improve pagination when having identical publication date 2025-01-18 16:59:48 -08:00
Frédéric Guillot 9c82e55b98 fix: do not strip tags in Atom entry title 2025-01-18 15:33:44 -08:00
Frédéric Guillot c9c422b135 feat: bump linter and minifier from ECMAScript 2017 to 2020 (ES11) 2025-01-18 11:32:50 -08:00
Julien Voisin 91f9a7650e refactor(js): add jshint check for strict comparison 2025-01-16 17:50:09 -08:00
jvoisin 605eeb4525 Fix a mistake introduced in f67d2e230b
Spotted by @michaelkuhn
2025-01-16 17:37:54 -08:00
Julien Voisin eb6991ae49 tests(js): improve .jshintrc 2025-01-15 18:43:03 -08:00
Cthulhux eac5d59f5b feat(locale): update German translation
New ntfy string translated
2025-01-15 17:15:55 -08:00
dependabot[bot] 3861f1aa67 build(deps): bump github.com/tdewolff/minify/v2 from 2.21.2 to 2.21.3
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.21.2 to 2.21.3.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.21.2...v2.21.3)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 20:50:56 -08:00
Julien Voisin 6af0cd5b5c refactor(js): simplify a bit keyboard_handler.js
- Mark two methods as static
- Use a `switch-case` construct instead of an Object and a loop.
2025-01-14 20:50:21 -08:00
Julien Voisin fccca0ce1e refactor(js): minor refactoring of touch_handler.js
- Mark a method as `static`
- use `Math.sqrt` instead of `Math.pow(…, 0.5)`
- Use `Math.sign` instead of a condition on the sign
- Inline some used-once variables
- Reduce the scope of some variables
2025-01-14 20:47:30 -08:00
Julien Voisin 8c3a9184ac refactor(js): remove an outdated check for {passive: true}
According to https://caniuse.com/?search=passive,
all browsers released after 2016 do support passive event listeners,
so no need to check for its presence.
2025-01-14 20:18:11 -08:00
Matthaiks 3b317b0b80 feat(locale): update Polish translation 2025-01-13 18:38:22 -08:00
Brieuc Dubois a702bf0342 feat(ntfy): Add option to use internal links 2025-01-13 10:36:49 -08:00
Frédéric Guillot e9520f5d1c fix(finder): do not add redirections to the list of subscriptions to avoid confusion 2025-01-12 17:09:32 -08:00
Frédéric Guillot f5fde36d45 fix(ui): reading preferences are reset if the form values are incorrect 2025-01-12 16:16:29 -08:00
Jake Walker 6cbe8c3a9d feat: add fix_ghost_cards rewrite rule 2025-01-12 14:43:27 -08:00
Julien Voisin 1e54a073d3 refactor(js): minor improvements in app.js
- Use `….classList.toggle` instead of `….classList.add`/`….classList.remove` in a condition
- Replace a `function()` with a `() =>`
- Use `Math.min` instead of a handwritten condition
2025-01-12 12:54:08 -08:00
dependabot[bot] bf66ef5a0f build(deps): bump github.com/coreos/go-oidc/v3 from 3.11.0 to 3.12.0
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.11.0 to 3.12.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.11.0...v3.12.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-12 12:22:16 -08:00
CaptainArk 9b25ea4ed6 feat(integration): add Discord integration 2025-01-12 12:18:57 -08:00
Julien Voisin f116f7dd6a test(sanitizer): add a fuzzer 2025-01-11 17:19:31 -08:00
dependabot[bot] e540547104 build(deps): bump golang.org/x/oauth2 from 0.24.0 to 0.25.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 17:12:57 -08:00
dependabot[bot] 5b1abbf340 build(deps): bump golang.org/x/net from 0.33.0 to 0.34.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 17:05:27 -08:00
dependabot[bot] 0d72cef82e build(deps): bump golang.org/x/crypto from 0.31.0 to 0.32.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/crypto/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:51:21 -08:00
Julien Voisin 79ec6ef81f feat(database): add optional build support for SQLite
Miniflux can be build with `go build -tags=sqlite` to test this. Note that
while it builds, it will fail at runtime, as some of the SQL used in miniflux is
postgresql-specific.
2024-12-29 15:09:26 -08:00
jvoisin 8d4954e29b Return an error should it happen in migrations 2024-12-29 11:51:47 -08:00
dependabot[bot] c81e17c20d build(deps): bump github.com/PuerkitoBio/goquery from 1.10.0 to 1.10.1
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-27 15:08:35 -08:00
Frédéric Guillot 5549f75dd7 fix(sanitizer): allow <hr> tags 2024-12-27 13:56:06 -08:00
Frédéric Guillot fc3c4873e5 doc: add note regarding validateUsername 2024-12-26 15:05:15 -08:00
Frédéric Guillot b87c547a07 ci: run Docker tests only when the Dockerfiles are modified 2024-12-26 14:58:32 -08:00
Frédéric Guillot 3466e9e2d6 ci: avoid building Linux packages for each pull-request 2024-12-26 14:57:52 -08:00
Julien Voisin 8df4b780a8 refactor(readingtime): replace whatlanggo with an ad-hoc implementation
The package `github.com/abadojack/whatlanggo` is unmaintained since 5 years, is
overkill for simply detecting CJK, and is quite slow.
2024-12-26 14:21:07 -08:00
Julien Voisin e22520fc55 feat: validate usernames upon creation
The validation doesn't apply to already created usernames.

This should close #925
2024-12-26 14:14:07 -08:00
Julien Voisin 518bc4d6ff refactor(database): add special handling for PostgreSQL-specific migrations 2024-12-26 14:09:37 -08:00
Julien Voisin 89620a7dd2 refactor(oauth2): no need to use io.WriteString when sha256 provides a way to obtain a sum in a single call 2024-12-26 10:39:55 -08:00
Julien Voisin bbfe39722a ci: tighten the CodeQL rules
- don't run CodeQL on test files
- don't run CodeQL if no `.go` nor `.js` file have been modified.
2024-12-26 10:33:41 -08:00
Julien Voisin f3989cdb2f ci: checkout before installing Go
Obtaining the code before deploying go allows better caching, as the go.sum
file becomes available.

See https://github.com/actions/setup-go/issues/281
2024-12-23 21:24:22 -08:00
Julien Voisin 195b75d185 refactor(rewriter): use custom title case converter implementation instead of golang.org/x/text/cases.Title()
The implementation is equivalent to
`cases.Title(language.English).String(strings.ToLower(…))`,
and this is the only place in miniflux where
"golang.org/x/text/cases" and "golang.org/x/text/language"
are (directly) used.

This reduces the binary size from 27015590 to
26686112 on my machine.

Kudos to https://gsa.zxilly.dev for making it straightforward to catch things
like this.
2024-12-23 21:16:02 -08:00
Julien Voisin f52411f734 ci: only run -race -cover on Ubuntu
The coverage information isn't used anywhere in the CI, so no need to have it
for every OS. As for `-race`, there is no point in using it everywhere, one
time should be enough, especially since it's taking a lot of time on Windows.
2024-12-23 11:45:45 -08:00
Julien Voisin b93543f416 feat: replace %{?systemd_requires} with %{?systemd_ordering}
As said [in the documentation](https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/#_dependencies_on_the_systemd_package):

> If the package wants to use systemd tools if they are available, but does not
want to declare a dependency, then the `%{?systemd_ordering}` macro MAY be used
as a weaker form of %{?systemd_requires} that only declares an ordering during
an RPM transaction.

See https://github.com/systemd/systemd/commit/2424b6bd716f0c1c3bf3406b1fd1a16ba1b6a556
and https://pagure.io/packaging-committee/issue/644 for more information.

And also use `--setopt=install_weak_deps=False` to avoid installing a lot of
useless dependencies.
2024-12-23 11:34:29 -08:00
Julien Voisin 057f760196 ci: don't run go vet ./... as it's run as part of golangci-lint
See https://golangci-lint.run/usage/linters/#govet
2024-12-22 22:04:00 -08:00
Julien Voisin 28fe053329 ci: don't specify languages for CodeQL
As stated in the [documentation](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#changing-the-languages-that-are-analyzed):

> CodeQL code scanning automatically detects code written in the supported languages.

This will also reduce the number of CodeQL jobs from two to one.

See #3029
2024-12-22 22:00:06 -08:00
Frédéric Guillot d345c87376 docs(changelog): update release notes for version 2.2.4 2024-12-20 13:05:52 -08:00
jvoisin bd91e5f320 Add more referer spoofing
Based on #2261. For moyu.im/jandan.net, see https://github.com/DIYgod/RSSHub/issues/11528
2024-12-20 11:53:38 -08:00
dependabot[bot] 276b2d8b0b build(deps): bump golang.org/x/net from 0.32.0 to 0.33.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 17:41:28 -08:00
Sevi.C bca9bea676 feat: add date-based entry filtering rules 2024-12-16 20:38:20 -08:00
dependabot[bot] 7346d751cc build(deps): bump library/alpine in /packaging/docker/alpine
Bumps library/alpine from 3.20 to 3.21.

---
updated-dependencies:
- dependency-name: library/alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 19:35:31 -08:00
jvoisin 7939b54341 Resize favicons to 32x32 to account of scaling
As suggested by @michaelkuhn in https://github.com/miniflux/v2/pull/2998#issuecomment-2546702212
2024-12-16 19:28:38 -08:00
jvoisin a06657b74d Factorise a line in internal/ui/static/js/app.js 2024-12-15 20:54:17 -08:00
jvoisin 2df59b4865 Refactor internal/reader/readability/testdata
- Use chained strings.Contains instead of a regex for
  blacklistCandidatesRegexp, as this is a bit faster
- Simplify a Find.Each.Remove to Find.Remove
- Don't concatenate id and class for removeUnlikelyCandidates, as it makes no
  sense to match on overlaps. It might also marginally improve performances, as
  regex now have to run on two strings separately, instead of both.
- Add a small benchmark
2024-12-15 20:52:32 -08:00
Julien Voisin 777d0dd248 feat: resize favicons before storing them
Some websites are using images of O(10kB) when not )O(100kB) for their
favicons. As miniflux only displays them with a 16x16 resolution, let's do our
best to resize them before storing them in the database. This should make
miniflux consume less bandwidth when serving pages, for the joy of mobile users
on a small data plan.

Of course, images that already are 16x16 aren't resized.
2024-12-15 20:47:19 -08:00
Julien Voisin cfda948c3a refactor(rewriter): avoid the use of regex in addDynamicImage
See https://dustri.org/b/parsing-noscript-tags-with-goquery.html for the whole
story.
2024-12-15 17:56:39 -08:00
jvoisin 14a6e8ed3a Factorise .pagination-next and .pagination-last together 2024-12-15 17:03:09 -08:00
jvoisin c3e842eba6 Remove -webkit-clip-path
https://caniuse.com/?search=clip-path says that `clip-path`
is supported since Safari 13.1
2024-12-15 17:03:09 -08:00
jvoisin fd9cfd757a Replace -ms-text-size-adjust with text-size-adjust
https://caniuse.com/?search=text-size-adjust says that
`ms-text-size-adjust` is supported in Edge.
2024-12-15 17:03:09 -08:00
Julien Voisin 945d436055 refactor(rewriter): replace regex with URL parsing for referrer override
No need for brittle regex when matching plain strings or domain names.
This should save some negligible amount of heap memory as well as
tremendously speeding up the matching.
2024-12-13 14:50:12 -08:00
Frédéric Guillot c3649bd6b1 refactor(rewrite): remove unused function arguments 2024-12-12 21:10:35 -08:00
Julien Voisin 6ad5ad0bb2 refactor(readability): various improvements and optimizations
- Replace a completely overkill regex
- Use `.Remove()` instead of a hand-rolled loop
- Use a strings.Builder instead of a bytes.NewBufferString
- Replace a call to Fprintf with string concatenation, as the latter are much
  faster
- Remove a superfluous cast
- Delay some computations
- Add some tests
2024-12-12 20:41:56 -08:00
Frédéric Guillot 113abeea59 test(rewrite): add unit test for referer rewrite function 2024-12-12 20:11:47 -08:00
Julien Voisin e6185b1393 refactor: use min/max instead of math.Min/math.Max
This saves a couple of back'n'forth casts.
2024-12-11 19:43:14 -08:00
Julien Voisin 1b0b8b9c42 refactor: use a better construct than doc.Find(…).First()
As mentioned in goquery's documentation (https://pkg.go.dev/github.com/PuerkitoBio/goquery#Single):

> By default, Selection.Find and other functions that accept a selector string
to select nodes will use all matches corresponding to that selector. By using
the Matcher returned by Single, at most the first match will be selected.
>
> The one using Single is optimized to be potentially much faster on large documents.
2024-12-11 19:40:55 -08:00
dependabot[bot] 68448b4abb build(deps): bump golang.org/x/crypto from 0.30.0 to 0.31.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-11 19:31:26 -08:00
Julien Voisin 3caa16ac31 refactor(processor): use URL parsing instead of a regex 2024-12-11 19:30:59 -08:00
Julien Voisin 637fb85de0 refactor(handler): delay store.UserByID as much as possible
In internal/reader/handler/handler.go:RefreshFeed, there is a call to
store.UserByID pretty early, which is only used for
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language)
Its only other usage is in processor.ProcessFeedEntries(store, originalFeed,
user, forceRefresh), which is pretty late in RefreshFeed, and only called if
there are new items in the feed. It makes sense to only fetch the user's
language if the error localization function is used.

Calls to `store.UserByID` take around 10% of the CPU time of RefreshFeed in my
profiling.

This commit also makes `processor.ProcessFeedEntries` take a `userID` instead
of a `user`, to make the code a bit more concise.

This should close #2984
2024-12-09 19:32:59 -08:00
Julien Voisin 02c6d14659 refactor(subscription): use strings.HasSuffix instead of a regex in FindSubscriptionsFromYouTubePlaylistPage 2024-12-09 17:19:28 -08:00
Julien Voisin 728423339a refactor(sanitizer): improve rewriteIframeURL()
- Use `url.Parse` instead of a regex, as this is much faster and way more robust
- Add support for Vimeo's Do Not Track parameter
2024-12-09 17:14:54 -08:00
Julien Voisin eed3fcf92a refactor(locale): delay parsing of translations until they're used
While doing some profiling for #2900, I noticed that
`miniflux.app/v2/internal/locale.LoadCatalogMessages` is responsible for more
than 10% of the consumed memory. As most miniflux instances won't have enough
diverse users to use all the available translations at the same time, it
makes sense to load them on demand.

The overhead is a single function call and a check in a map, per call to
translation-related functions.
2024-12-09 17:05:14 -08:00
Qeynos d5cfcf8956 feat(locale): update Chinese translation 2024-12-08 17:06:29 -08:00
Julien Voisin dea46ac0ea refactor: optimize sanitizeAttributes
- Use string concatenation instead of `Sprintf`, as this is much faster, and the
  call to `Sprintf` is responsible for 30% of the CPU time of the function
- Anchor the youtube regex, to allow it to bail early, as this also account for
  another 30% of the CPU time. It might be worth chaining calls to `TrimPrefix`
  and check if the string has been trimmed instead of using a regex, to speed
  things up even more, but this needs to be benchmarked properly.
2024-12-08 14:42:18 -08:00
Julien Voisin 30c44380e0 refactor: get rid of numberOfPluralFormsPerLanguage test-only variable
The `numberOfPluralFormsPerLanguage` variable is only used for tests, so it
should be declared in the test file.
2024-12-08 14:39:11 -08:00
Julien Voisin a913f3f75f feat(rewrite)!: remove parse_markdown rewrite rule
It was added in 2022 by #1513, to support blog.laravel.com, which has
since switched to HTML. The Atom 0.3/1.0, RSS 1.0/2.0, RDF, and JSON formats
don't support markdown in their spec, and any website serving it there should
be considered as buggy and fixed.

This shaves off 2MB from the miniflux binary, which is quite steep for a
feature that nobody is/should be using, and remove a dependency which is always
a good thing.
2024-12-08 14:34:47 -08:00
AiraNadih c1ef986cab fix(consistency): align feed modification behavior between API and UI 2024-12-08 10:49:14 -08:00
Julien Voisin 2671f57edd refactor(readability): simplify the regexes in internal/reader/readability/readability.go
- Use strings.ToLower() instead of having case-insensitive regex
- Remove overlapping words in the regex
- Split a condition to increase readability
2024-12-07 16:56:19 -08:00
jvoisin 2f56ebd3a6 Remove a now-useless function 2024-12-07 16:50:18 -08:00
jvoisin 059f5c0905 Inline a condition 2024-12-07 16:50:18 -08:00
jvoisin 58178d90cb Refactor Sanitize
- Use `token.String()` instead of `html.EscapeString(token.Data)`
- Refactor conditions to highlight their similitude, enabling further
  refactoring

This refactoring brings forth at least one bug: `tagStack` is never emptied.
2024-12-07 16:50:18 -08:00
jvoisin cc885bbabb config.Opts is guaranteed to never be nil 2024-12-07 16:50:18 -08:00
jvoisin 0e185849b4 Google+ isn't a thing anymore 2024-12-07 16:50:18 -08:00
jvoisin d0984f29da Simplify isValidTag 2024-12-07 16:50:18 -08:00
jvoisin 902ca63c45 Inline a function and fix a bug in it
The `isAnchor` function's first parameter was always `a`, instead of being
passed `tagName`. As this function is a single line and was only called in a
single place, it can be inlined.
2024-12-07 16:50:18 -08:00
jvoisin 2314500515 Merge two conditions 2024-12-07 16:50:18 -08:00
jvoisin 787d373211 Change the scope of a variable 2024-12-07 16:50:18 -08:00
Julien Voisin fefbf2c935 refactor(processor): improve the rewrite URL rule regex
- Use `[^"]` instead of `.`, to help the regex engine to determine boundaries,
  instead of having it bruteforce its way to find them
- Use `+` instead of `*`, as empty rules don't make sense
2024-12-07 16:35:51 -08:00
Julien Voisin bfb429b919 refactor(sanitizer): optimize internal/reader/sanitizer/strip_tags.go
- Use strings instead of doing string->bytes->string
- Use a strings.Builder to build the output
2024-12-07 16:31:48 -08:00
Julien Voisin 331c831c23 refactor(sanitizer): simplify hasRequiredAttributes
This function takes around 1.5% of the total CPU time on my instance, and most
of it is spent in `mapassign_faststr` to initialize the `map`. This is replaced
with a switch-case construct, that should be both significantly faster as well
as pretty dull in term of memory consumption.
2024-12-07 16:30:15 -08:00
Julien Voisin 92a49d7e69 refactor(sanitizer): micro-optimizations of internal/reader/sanitizer/srcset.go
- Pre-allocate a slice
- Inline a local variable
- Remove a superfluous call to `strings.TrimSpace`
- Simplify some conditions via a switch-case construct
2024-12-07 16:27:56 -08:00
mrchi 8cdf76df69 fix(linting): remove unnecessary blank line in PushEntries function 2024-12-07 16:19:53 -08:00
mrchi 7bc0bffd85 feat(apprise): update SendNotification to handle multiple entries and add logging 2024-12-07 16:19:53 -08:00
mrchi 3a18e5d205 feat(apprise): add title in notification request body 2024-12-07 16:19:53 -08:00
Gabe Cook c3ca603960 fix: load icon from site URL instead of feed URL 2024-12-07 16:06:26 -08:00
telnet23 7e2b50efee feat: optionally fetch watch time from YouTube API instead of website 2024-12-07 16:00:35 -08:00
Gabe Cook b61ee15c1b fix: feed icon from xml ignored during force refresh 2024-12-07 15:59:49 -08:00
Cthulhux b7b0ccbf4b feat(locale): update German translation to use Readeck URL 2024-12-06 15:13:21 -08:00
dependabot[bot] 9c5228b2ee build(deps): bump golang.org/x/net from 0.31.0 to 0.32.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/net/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-04 18:29:21 -08:00
dependabot[bot] 53beec685b build(deps): bump golang.org/x/term from 0.26.0 to 0.27.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/term/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-04 16:32:39 -08:00
Mohit Raj b1fb8be185 feat(locale): update translations to clarify readeck url instead of readeck api endpoint 2024-12-03 04:50:34 -08:00
Anshul Gupta 3a966c6ce5 fix: replace timezone function call with view
The `pg_timezone_names` view was added in 8.2.
It should be equivalent to the function query.
See: https://pgpedia.info/p/pg_timezone_names.html

This small change allows `miniflux` to run on postgres-compatible
databases like CockroachDB, which don't have this function.
2024-12-03 01:44:48 -08:00
dependabot[bot] b2e702218d build(deps): bump github.com/tdewolff/minify/v2 from 2.21.1 to 2.21.2
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.21.1 to 2.21.2.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.21.1...v2.21.2)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 01:33:05 -08:00
Gabe Cook 30c2e09a56 chore: remove blog.laravel.com rewrite rule 2024-12-03 01:21:42 -08:00
3zero2 c6c71c58b8 feat: add predefined scraper rules for arstechnica.com 2024-11-14 17:47:31 -08:00
Julien Voisin 6eb1f25a53 feat: only show the commit URL if it's not empty on /about 2024-11-12 20:09:48 -08:00
AiraNadih f0fe91172f feat(mediaProxy): update predefined referer spoofing rules for restricted media resources 2024-11-12 19:47:23 -08:00
Cthulhux c3016a4c55 fix: fix grammar in pull-request template 2024-11-11 19:55:01 -08:00
Cthulhux 3a028a0669 feat(locale): update German translations 2024-11-11 19:53:58 -08:00
Frédéric Guillot da951164d5 docs(changelog): update release notes for version 2.2.3 2024-11-10 15:53:03 -08:00
dependabot[bot] a0988f6c16 build(deps): bump golang.org/x/net from 0.30.0 to 0.31.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-09 21:38:00 -08:00
dependabot[bot] fddc861e41 build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 20:56:26 -08:00
dependabot[bot] 9288001f09 build(deps): bump golang.org/x/text from 0.19.0 to 0.20.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 20:55:59 -08:00
dependabot[bot] da261a5fc7 build(deps): bump golang.org/x/term from 0.25.0 to 0.26.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/term/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 19:47:31 -08:00
dependabot[bot] edeb5f0366 build(deps): bump golang.org/x/oauth2 from 0.23.0 to 0.24.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-07 21:50:20 -08:00
Frédéric Guillot 65ff328804 fix: show only one player when there are several audio/video enclosures 2024-11-03 21:28:26 -08:00
John Tiesselune 7759ea1b43 feat(integration-shiori): updated shiori API to new endpoint for login/bookmark 2024-11-02 20:19:14 -07:00
Frédéric Guillot 7c5d6cf35f feat(mediaproxy): pass original filename in Content-Disposition header
When you download/save proxified media, the original filename is lost. That
information could be retained by passing a header `Content-Disposition: inline;
filename="ORIGNAL_FILENAME.EXT"` when serving the media file. The requested URL
would still be obfuscated, but if the client downloads the file it'll use that
original filename.
2024-10-30 20:44:19 -07:00
Frédéric Guillot 39cc1887ea fix: unable to change password due to a typo in SQL parameter 2024-10-30 19:58:37 -07:00
AiraNadih b0a3b4d5d9 style(mediaProxy): format with gofmt to pass linter checks 2024-10-30 19:50:12 -07:00
AiraNadih 469f23968e feat(mediaProxy): implement referer spoofing for restricted media resources 2024-10-30 19:50:12 -07:00
Frédéric Guillot 051bdecabd docs(changelog): update release notes for version 2.2.2 2024-10-29 20:08:17 -07:00
dependabot[bot] fe10d4302d build(deps): bump github.com/tdewolff/minify/v2 from 2.20.37 to 2.21.1
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.37 to 2.21.1.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.37...v2.21.1)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 17:13:38 -07:00
Matthaiks b9dfd5bf6d feat(locale): update Polish translations 2024-10-28 17:12:48 -07:00
Frédéric Guillot 51030ef1a8 feat(webauthn): show help message regarding username and non-discoverable credentials
The username is required for non-resident keys, but it's not necessary for discoverable credentials like Passkeys.
2024-10-26 21:49:23 -07:00
Frédéric Guillot 2bcc4b8399 fix(webauthn): add backup eligibility flag workaround to avoid a 401
Since go-webauthn v0.11.0, the backup eligibility flag is strictly validated, but Miniflux does not store this flag.

This workaround to set the flag based on the parsed response, and avoid "BackupEligible flag inconsistency detected during login validation" error.

See https://github.com/go-webauthn/webauthn/pull/240
2024-10-26 18:36:38 -07:00
Matthaiks ea4d0a4f72 feat(locale): update Polish translation 2024-10-22 20:16:58 -07:00
Julien Voisin 5c5ad19c43 feat: replace xurls third-party module with an ad-hoc regexp 2024-10-21 20:48:12 -07:00
Frédéric Guillot 191f3a7ad7 feat(rss): calculate hash based on item title/content for feeds without GUID and link 2024-10-18 18:37:38 -07:00
Shaolong Chen 366928b35d feat(integration): add cubox integration
Signed-off-by: Shaolong Chen <shaolong.chen@outlook.it>
2024-10-18 18:06:09 -07:00
Frédéric Guillot 3b654fefa7 feat(api): rename integrations status endpoint 2024-10-17 21:29:44 -07:00
AiraNadih 0adbcc3a04 feat(api): add endpoint for user integration status 2024-10-17 20:59:05 -07:00
Frédéric Guillot 7fdb450446 Update issue templates 2024-10-17 20:27:10 -07:00
dependabot[bot] 86285f5a05 build(deps): bump github.com/yuin/goldmark from 1.7.7 to 1.7.8
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.7 to 1.7.8.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.7...v1.7.8)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 21:09:35 -07:00
dependabot[bot] 8a4b4e459e build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.4 to 1.20.5.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 16:42:00 -07:00
dependabot[bot] 879446bdb2 build(deps): bump github.com/yuin/goldmark from 1.7.6 to 1.7.7
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.6 to 1.7.7.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.6...v1.7.7)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 16:36:56 -07:00
dependabot[bot] ef633dc427 build(deps): bump github.com/yuin/goldmark from 1.7.4 to 1.7.6
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.4 to 1.7.6.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.4...v1.7.6)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 18:18:38 -07:00
July 86c0cc61ba feat: set entry URL to rewritten URL if a rewrite rule is defined 2024-10-13 21:21:28 -07:00
dependabot[bot] ffe3ed4b9a build(deps): bump github.com/andybalholm/brotli from 1.1.0 to 1.1.1
Bumps [github.com/andybalholm/brotli](https://github.com/andybalholm/brotli) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/andybalholm/brotli/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: github.com/andybalholm/brotli
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-11 20:13:37 -07:00
Frédéric Guillot 5c4df786de fix: avoid panic in IsRateLimited() function 2024-10-06 21:34:23 -07:00
Qeynos ee8c6621e9 feat(locale): update zh_CN translations 2024-10-06 20:12:00 -07:00
Matthaiks f748513df6 feat(locale): update Polish translations 2024-10-06 12:56:49 -07:00
Frédéric Guillot e1050e21b5 feat: take Retry-After header into consideration for rate limited feeds 2024-10-05 22:26:05 -07:00
Frédéric Guillot e555e442fb feat: add new settings option to allow external fonts 2024-10-05 21:21:12 -07:00
Frédéric Guillot 600dea6ce5 feat(client): add custom_js field to Go API client 2024-10-05 18:37:34 -07:00
milhnl e07203ad46 feat: add custom user JavaScript 2024-10-05 16:54:11 -07:00
Frédéric Guillot f16735fd6d feat: update feed icon during force refresh 2024-10-04 20:51:40 -07:00
Scott Leggett 562a7b79a5 fix: update Last-Modified if it changes in a 304 response
When a server returns a 304 response with a strong validator, any other
stored fields must be updated if they are also present in the response.

This behaviour is described in RFC9111, sections 3.2 and 4.3.4.
2024-10-04 17:47:48 -07:00
Scott Leggett cb610230d9 chore: update test case comment
The updated comment reflects a better understanding of the RFCs.
2024-10-04 17:47:48 -07:00
dependabot[bot] 628b2b388d build(deps): bump golang.org/x/net from 0.29.0 to 0.30.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/net/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-04 17:19:04 -07:00
Frédéric Guillot faa70f3019 docs(changelog): update release notes for version 2.2.1 2024-09-27 19:20:06 -07:00
Frédéric Guillot cfe410f202 refactor: split processor package into smaller files 2024-09-22 18:54:19 -07:00
Qeynos c2ac2bfb83 feat: use Bilibili API instead of web scraping to get video watch time 2024-09-22 18:05:43 -07:00
Dark Dragon c326d5574b build: Bump devcontainer version to go 1.23 2024-09-21 20:56:56 -07:00
dependabot[bot] f1c8c060c0 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.3 to 1.20.4.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.3...v1.20.4)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 16:48:06 -07:00
Victorhck 6944fb1e50 feat(locale): update Spanish translations 2024-09-12 21:26:02 -07:00
Frédéric Guillot 7d21298fab fix(mediaproxy): forward client user-agent to origin to bypass bot protection 2024-09-11 21:01:56 -07:00
Frédéric Guillot 95201fc5cf build(deps): bump github.com/go-webauthn/webauthn from 0.10.2 to 0.11.2 2024-09-07 16:03:05 -07:00
dependabot[bot] 5e335995e1 build(deps): bump github.com/PuerkitoBio/goquery from 1.9.2 to 1.10.0
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.9.2 to 1.10.0.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.9.2...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-07 15:51:55 -07:00
Frédéric Guillot 70f126fc5a build: update go.mod to Go 1.23 2024-09-07 15:27:20 -07:00
Michiel Janssens 38cdc4d3df feat(locale): update Dutch translations 2024-09-05 20:16:43 -07:00
dependabot[bot] 4ab1cdd2e9 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.3/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.2...v1.20.3)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-05 19:41:17 -07:00
dependabot[bot] f3e48505df build(deps): bump golang.org/x/net from 0.28.0 to 0.29.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/net/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-05 19:40:53 -07:00
dependabot[bot] 60c75ab3b6 build(deps): bump golang.org/x/term from 0.23.0 to 0.24.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/term/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 20:54:05 -07:00
dependabot[bot] 349f040921 build(deps): bump golang.org/x/oauth2 from 0.22.0 to 0.23.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 20:28:21 -07:00
Ztec 74376cd33c fix: remove progression save on shared entry
Shared entry does not link to any user and therefore should not display
any saved progression. Curiously, the progression of a user (the one that shared ?)
was still integrated in the page. This does not make sens regarding the sharing
feature itself. It is also a leak of user personal information onto a public page.

I simply removed the data from the template when the user object is not present.
I tested the change on "regular" entry page, ensuring the save progression feature
still works, and on shared page checking if any error happened in the JavaScript console.
Everything seems in order.
2024-09-03 20:50:04 -07:00
Qeynos 2a4d2985c4 feat(locale): update zh_CN translations 2024-09-03 19:45:20 -07:00
Phantop 907941394b feat: add pagination to shared entries listing 2024-09-02 21:27:17 -07:00
Kierán Meinhardt 88ea0ade3e feat(locale): add dummy translations for menu.show_only_starred_entries 2024-09-02 21:23:17 -07:00
Kierán Meinhardt fcf9fde118 feat(locale): add translations for menu.show_only_starred_entries 2024-09-02 21:23:17 -07:00
Kierán Meinhardt 5c38688783 feat: add button to show only starred entries per category
fixes #1468
2024-09-02 21:23:17 -07:00
John e0850fc648 feat(locale): update Ukrainian translations 2024-09-02 19:32:03 -07:00
dependabot[bot] 8708a109b3 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.1 to 1.20.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.2/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.1...v1.20.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-29 05:09:52 -07:00
dependabot[bot] 0fe787bb93 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.0 to 1.20.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.1/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.0...v1.20.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-22 14:31:21 -07:00
Jonatas Baldin 2dffcfeadc fix: add datasource variable and upgrade depecrated panels on the grafana dashboard 2024-08-22 14:29:50 -07:00
Frédéric Guillot 4bbc12e3b2 fix: use root URL to generate absolute proxy URL
When using `BASE_URL` with a subfolder, the root URL must be used to
avoid base folder appearing twice in the generated URL.
2024-08-19 20:44:46 -07:00
Frédéric Guillot 3e0e8dda2b docs(changelog): update release notes for v2.2.0 2024-08-18 12:48:00 -07:00
Frédéric Guillot eb4bca6eb7 fix: store.GetEnclosure() should return nil if no rows are returned 2024-08-18 12:41:30 -07:00
Pontus Jensen Karlsson 810b351772 feat: add API routes /v1/enclosures/{enclosureID} 2024-08-18 11:53:19 -07:00
Alexandros Kosiaris 89ff33ddd0 fix(client): Return nil and error if endpoint is empty string
Why:
Passing an empty string as an endpoint to Client when instantiating a
new client might seem like something that should never happen but I
managed to trigger it while parsing some input files to register feeds
in bulk.

What:
In the execute() function, check early if the endpoint is "" and then
return immediately nil and a new error, named ErrEmptyEndpoint with a
descriptive string
2024-08-18 11:35:45 -07:00
Frédéric Guillot f3a5a3ee14 fix(fever): correct sorting direction when using max_id argument 2024-08-17 18:08:01 -07:00
Frédéric Guillot e98e16e45a build: add sha256 checksum file for published binaries 2024-08-15 17:24:28 -07:00
Frédéric Guillot eb057d0415 build: bump Alpine Linux build image to v3.20 2024-08-15 16:48:26 -07:00
Frédéric Guillot fa51c3ead7 chore: avoid using legacy key/value format in Dockerfile 2024-08-15 16:39:43 -07:00
Frédéric Guillot 56d7e4d5e9 build: update GitHub Actions to Go 1.23 2024-08-15 16:34:00 -07:00
Frédéric Guillot cc94ab704a feat: validate OAUTH2_PROVIDER value 2024-08-14 19:09:14 -07:00
Michael Kuhn 9b8eabf036 feat: change log level to info when running migrations
When upgrading my installation, I noticed that `miniflux -migrate` does
not provide any output by default. This can be a bit confusing since one
cannot be sure whether anything has happened. Use `Info` instead of
`Debug` to provide some basic output by default.
2024-08-14 17:07:27 -07:00
dependabot[bot] a8ac3dec47 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.1 to 1.20.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.1...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-14 17:04:43 -07:00
Finn 6feee555ba feat: allow customizing the display name of the OpenID Connect provider 2024-08-12 22:05:15 -07:00
Piper McCorkle ee926e73cb feat: add license info to js, for LibreJS compatibility
[LibreJS][0] is a browser extension developed by GNU which ensures only Free (libre) JavaScript is run. To determine whether given JavaScript is Free, LibreJS consults metadata included in the JavaScript file. Since Miniflux is Free Software, getting its JavaScript to work when LibreJS is installed is just a matter of adding license metadata to the returned JavaScript source.

[0]: https://www.gnu.org/software/librejs/index.html
2024-08-12 20:43:18 -07:00
Pontus Jensen Karlsson ade412f453 fix: Honor hide_globally when creating a new feed through the api
TestGetGlobalEntriesEndpoint was failing because CreateFeed ignored HideGlobally, this fixes that.
2024-08-12 20:20:44 -07:00
Pontus Jensen Karlsson 6fb7e84ce1 feat: API: Allow filtering entries on globally_hidden
Currently there's no way through the API to mimic the Unread page of the client.
This is now possible by filtering on globally_visible=true and status=unread.
2024-08-12 20:20:44 -07:00
Finn 770cc1dbb3 feat: Add option to disable local auth form 2024-08-12 19:27:08 -07:00
dependabot[bot] 59dac15bdf build(deps): bump golang.org/x/net from 0.27.0 to 0.28.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/net/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 21:46:15 -07:00
dependabot[bot] da6aa36758 build(deps): bump golang.org/x/oauth2 from 0.21.0 to 0.22.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 16:55:38 -07:00
dependabot[bot] 2a22fe6b75 build(deps): bump golang.org/x/crypto from 0.25.0 to 0.26.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 15:28:35 -07:00
Qeynos bcbf9f4025 feat: add FETCH_BILIBILI_WATCH_TIME config option 2024-08-01 19:52:31 -07:00
Qeynos 569529d73b feat(locale): update zh_CN translations 2024-07-31 19:06:36 -07:00
Qeynos 31cb06026d feat(locale): update zh_CN translations 2024-07-30 20:30:47 -07:00
Frédéric Guillot d048d59d39 fix: use BASE_URL instead of r.Host to generate absolute media proxy URL 2024-07-29 16:04:31 -07:00
Loïc Doubinine 4f55361f5f feat: mark media as read when playback reaches 90% 2024-07-28 12:29:45 -07:00
Frédéric Guillot 37309adbc0 fix: do not alter the original URL if there is no tracker parameter 2024-07-25 22:10:28 -07:00
Frédéric Guillot 92f3dc26e4 feat: add support for aside HTML element in entry content 2024-07-25 21:11:37 -07:00
Frédéric Guillot f6dc952551 feat: add support for base element when discovering feeds 2024-07-25 20:54:51 -07:00
Frédéric Guillot 29387f2d60 feat: implement base element handling in content scraper 2024-07-25 20:36:56 -07:00
Frédéric Guillot c0f6e32a99 feat: remove well-known URL parameter trackers 2024-07-19 21:35:47 -07:00
Frédéric Guillot 11cafec863 fix: align pagination correctly on small screens with non-English text 2024-07-19 18:32:37 -07:00
Wojtek 8cfe77a3cd build: publish OCI images only if PUBLISH_DOCKER_IMAGES=true 2024-07-17 18:28:16 -07:00
Thiago Perrotta 8d4d092cd7 docs: update links to filtering rules 2024-07-16 19:30:49 -07:00
Frédéric Guillot 968355f9b9 feat(integration): add ntfy integration 2024-07-13 17:51:17 -07:00
Frédéric Guillot 3ca52c7f7f feat(locale): update French translations 2024-07-13 13:18:31 -07:00
Frédéric Guillot 2e856a6bf0 fix(integration): define content encoding explicitly when sending article body to Readeck 2024-07-13 13:07:50 -07:00
Frédéric Guillot 36c25e7689 refactor: simplify Youtube feeds discovery 2024-07-13 12:17:13 -07:00
Frédéric Guillot cb97d4a1a8 feat: remove YouTube video page subscription finder because meta[itemprop="channelId"] no longer exists 2024-07-13 11:11:50 -07:00
Frédéric Guillot 79ea9e28b5 fix: panic during YouTube channel feed discovery
Regression introduced in commit e54825b
2024-07-13 10:18:15 -07:00
wangb f847c3e754 fix: video poster image URL is encoded twice when using MEDIA_PROXY_MODE=all 2024-07-13 09:20:55 -07:00
WShihan 4ca19d123a feat(locale): update Chinese translations 2024-07-10 19:30:04 -07:00
dependabot[bot] 90ef864edd build(deps): bump github.com/tdewolff/minify/v2 from 2.20.36 to 2.20.37
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.36 to 2.20.37.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.36...v2.20.37)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 19:07:48 -07:00
Taylan Tatlı 01133c586f feat(locale): update Turkish translations 2024-07-10 19:07:28 -07:00
Frédéric Guillot b683756d8e Update ChangeLog 2024-07-09 20:51:30 -07:00
dependabot[bot] 3dfc70cee6 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.35 to 2.20.36
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.35 to 2.20.36.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.35...v2.20.36)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 20:03:17 -07:00
dependabot[bot] 91b4a7d35f build(deps): bump github.com/coreos/go-oidc/v3 from 3.10.0 to 3.11.0
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.10.0...v3.11.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 17:13:45 -07:00
Paul Esch-Laurent 2fbe2df086 fix: <img> aspect ratio w/ height: auto
Complement with `max-width: 100%` with a `height: auto` to preserve `<img>` aspect ratios, particularly when it's not wrapped in a block parent e.g. `<p>` or `<figure>` most commonly.

Related: https://www.smashingmagazine.com/2020/03/setting-height-width-images-important-again/
2024-07-08 17:13:20 -07:00
dependabot[bot] 964698f363 build(deps): bump golang.org/x/net from 0.26.0 to 0.27.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/net/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-05 21:08:51 -07:00
dependabot[bot] e34af65ae9 build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-05 20:39:28 -07:00
dependabot[bot] e99a675912 build(deps): bump golang.org/x/term from 0.21.0 to 0.22.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/term/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 16:04:01 -07:00
Frédéric Guillot d96ad4ddef feat(locale): update French translations 2024-07-04 16:03:30 -07:00
Frédéric Guillot 4272932402 Update GitHub PR template 2024-07-04 13:12:10 -07:00
Krish Mamtora a60996e666 Update the expected rule template for the rule validator 2024-07-04 13:07:40 -07:00
Krish Mamtora a09ddbbaf4 Remove carriage returns to sanitizer strings from windows 2024-07-04 13:07:40 -07:00
Danila Gorelko 92db691344 Add Betula integration 2024-07-04 12:59:47 -07:00
Frédéric Guillot a334c8e691 locale: update French translation 2024-07-03 10:33:21 -07:00
Scott Leggett bf1c851093 fetcher: use ETag as a stronger validator than Last-Modified
As per the MDN article on HTTP caching:

  During cache revalidation, if both If-Modified-Since and If-None-Match
  are present, then If-None-Match takes precedence for the validator.

  https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching

Previously Miniflux would consider a resource unmodified if the
Last-Modified header had not changed, even if the ETag had changed.

With this commit, Miniflux will consider a resource modified if the ETag
header has changed, even if Last-Modified has not.

This fixes Bug 1 in https://rachelbythebay.com/w/2024/06/11/fsr/
2024-07-02 22:05:49 -07:00
Scott Leggett c787bb5b48 fetcher: add tests for IsModified behaviour
In particular, add a failing test for the case where ETag changes but
Last-Modified does not.
2024-07-02 22:05:49 -07:00
privatmamtora 1a81866bb9 Add global block and keep filters 2024-07-02 21:03:49 -07:00
dependabot[bot] c4278821cb build(deps): bump github.com/tdewolff/minify/v2 from 2.20.34 to 2.20.35
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.34 to 2.20.35.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.34...v2.20.35)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-02 18:32:05 -07:00
Ztec 4498ba10e8 Fix: Integration with Pinboard: Preserve Existing Bookmarks
The Issue:
When saving an entry that is already bookmarked on Pinboard,
Miniflux was overriding all existing data on Pinboard. This action
removed any extended content or, worse, changed the private settings
to public, making previously private bookmarks publicly available.

The Fix:
Now, upon saving an entry as a bookmark, I first fetch it. If it
already exists, I apply the necessary modifications (adding tags and any state)
that Miniflux would have normally done, then add it again. This way, no
data is lost in the process. Pinboard has a stable API, so I don't anticipate
any new fields being added soon.

I manually tested the integration by hitting the save button in the following situations:
- Entry URL does not exist on Pinboard:
  - Bookmark is properly added on Pinboard with tags and "to read" status according to Miniflux settings.
- Entry URL already exists on Pinboard:
  - Existing data remains unchanged.
  - Tags from Miniflux settings are properly added to the bookmark.
  - "To read" status is set to yes when the option is checked in Miniflux. Nothing is changed otherwise.
2024-06-28 20:27:52 -07:00
Wojtek a46e702536 Add navigation to last/first page 2024-06-28 20:19:38 -07:00
dependabot[bot] f0e8323f19 build(deps): bump github.com/yuin/goldmark from 1.7.3 to 1.7.4
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.3 to 1.7.4.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.3...v1.7.4)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-25 16:11:25 -07:00
dependabot[bot] a0106c9ffc build(deps): bump github.com/yuin/goldmark from 1.7.2 to 1.7.3
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.2...v1.7.3)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 20:21:48 -07:00
emv33 f98d5de484 Telegram: add feed name to message
39d752c removed a link to the feed name to solve a web preview issue. This change brings back the feed name without the link, thus restoring the feed name without bringing back the issue.

Fixes #2620
2024-06-21 14:23:30 -07:00
JohnnyJayJay ee5e18ea9f sanitizer: add support for HTML hidden attribute
This commit adjusts the `Sanitize` function to skip tags with the
`hidden` attribute, similar to how it skips blocked tags and their
contents.
2024-06-21 14:00:40 -07:00
dependabot[bot] 3ef2522c62 build(deps): bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 18:23:26 -07:00
dependabot[bot] 839c4ad044 build(deps): bump github.com/yuin/goldmark from 1.7.1 to 1.7.2
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.1...v1.7.2)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-14 16:43:20 -07:00
Ztec 9f3a8e7f1b Request builder: Allow the use of insecure TLS ciphers when Allow self-signed or invalid certificates is used
Some server on the wild are badly configured. Either by mistake or lack
of maintenance. Safe and unsafe Ciphers change overtime based on new
discoveries.

This proposition will include considered unsafe ciphers when `Allow self-signed or invalid certificates` is used.
It could be put into a separate option but, I felt this could fit in.

fix #2671
2024-06-13 20:23:37 -07:00
Ztec e54825bf02 Improve YouTube page feed detection
In order to be more resilient to YouTube URLs variation and
to address this feature_request: https://github.com/miniflux/v2/issues/2628
I've reworked a bit the way the YouTube feed extraction is done.

I've kept all the `FindSubscriptionsFromYouTube*` in order
to keep all the existing unit tests as-is ensuring little to no
regressions. By doing so, I had to call twice `youtubeURLIDExtractor`.
Small performance penalty for peace of mind in my opinion.

`youtubeURLIDExtractor` is made in a way only one kind
of page can be detected at a time. This mean I can
solve the "video in a playlist" feature_request
by prioritizing the playlist ID over the Video ID

Also, by using `url.Parse()` to get ids, it's safer
to url mangle and variation. The most common variation
being the `t=42` parameters that start the playback
at a given position. Previously, this kind of url
would not be detected as "YouTube URL".

I deliberately ignored the url parsing error
to keep previous behavior (skip the YouTube analysis and follow with the other analysis)

I also tried to keep debug logs the same as before as much as I could.

I manually tested all the YouTube cases (video,channel,playlist)
and they all work as expected except for the video. But this one
does not work either on main. The `meta` html tag that was searched for
does not seem to exist anymore.

fix: #2628
2024-06-13 20:18:47 -07:00
Ztec 07f6d397d4 Fix Playback speed indicator precision
The original idea was to have two digit precision at all time
in order to ensure the length of the string is always the same.
This prevents the UI button to move when pressed.
I completely missed the first press as the precision was not right
upon first click.
2024-06-13 20:13:07 -07:00
Ztec f33e76eb8c Fix Playback speed indicator on shared entries
On shared entries, there is no speed configured as this
is bound to the user. Shared entries are displayed without user config.

I've changed the default view to reflect the
actual default playback speed in this case. 1x.
2024-06-13 20:11:33 -07:00
dependabot[bot] 84e97826d8 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.33 to 2.20.34
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.33 to 2.20.34.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.33...v2.20.34)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 20:24:20 -07:00
x 839fc3843a Add pitchfork.com scraping rule 2024-06-10 21:08:59 -07:00
x 0bab8fac8e Update theverge.com rewrite rule: fix duplicate image
See: https://github.com/miniflux/v2/issues/1979
2024-06-10 21:08:59 -07:00
dependabot[bot] 0cf1a40276 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.32 to 2.20.33
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.32 to 2.20.33.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.32...v2.20.33)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-09 20:38:34 -07:00
dependabot[bot] 91479bc0ee build(deps): bump golang.org/x/net from 0.25.0 to 0.26.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/net/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-04 20:02:27 -07:00
dependabot[bot] 251821289c build(deps): bump golang.org/x/oauth2 from 0.20.0 to 0.21.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-04 19:40:09 -07:00
dependabot[bot] cac0bc682f build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-04 19:39:49 -07:00
dependabot[bot] a733c14c61 build(deps): bump golang.org/x/term from 0.20.0 to 0.21.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/term/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-04 19:15:01 -07:00
Ankit Pandey b68b05c64c reader/processor: error out for improper rewrite regexp
It's possible to specify a rewrite regex that validates but doesn't compile such
as:

    rewrite("(((unmatched-capture-group"|"rewrite)))")

In case we encounter one, exit early instead of letting the server panic.
2024-06-01 10:37:02 -07:00
Frédéric Guillot 5ce3f24838 googelreader: set CrawlTimeMsec at the correct precision
Fixes #2669

Fixes #2670
2024-05-29 21:54:02 -07:00
dependabot[bot] 48ddc02ba8 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.30 to 2.20.32
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.30 to 2.20.32.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.30...v2.20.32)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 15:46:53 -07:00
dependabot[bot] fe9f1bba16 build(deps): bump library/alpine in /packaging/docker/alpine
Bumps library/alpine from 3.19 to 3.20.

---
updated-dependencies:
- dependency-name: library/alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 15:44:57 -07:00
Krish Mamtora 740fa4a5d2 Add missing properties when reloading page after error 2024-05-27 15:37:53 -07:00
dependabot[bot] 8a38f54ef5 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.25 to 2.20.30
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.25 to 2.20.30.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.25...v2.20.30)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 19:16:11 -07:00
Zhizhen He ae432bc9c6 reader/readingtime: fix incorrect package name 2024-05-21 18:12:24 -07:00
dependabot[bot] 96f7e8bae0 ---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-21 17:48:00 -07:00
rootknight 1f35ed1675 ui: add viewport-fit=cover 2024-05-19 10:39:34 -07:00
dependabot[bot] d6deac1810 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.21 to 2.20.24
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.21 to 2.20.24.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.21...v2.20.24)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-18 08:39:43 -07:00
Frédéric Guillot b692768730 packaging: fix failed to solve: arm64v8/golang:1.22-bookworm 2024-05-17 21:07:40 -07:00
dependabot[bot] 2178580a75 build(deps): bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 17:59:04 -07:00
dependabot[bot] b52f61cc77 build(deps): bump github.com/tdewolff/minify/v2 from 2.20.20 to 2.20.21
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.20 to 2.20.21.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.20...v2.20.21)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 17:58:41 -07:00
dependabot[bot] 3388f8e376 Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-09 21:37:53 -07:00
dependabot[bot] 83ceb20c1c Bump golang.org/x/net from 0.24.0 to 0.25.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/net/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 16:45:02 -07:00
dependabot[bot] c06850ca34 Bump golang.org/x/oauth2 from 0.19.0 to 0.20.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 16:22:00 -07:00
dependabot[bot] d856c02fbb Bump golang.org/x/crypto from 0.22.0 to 0.23.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/crypto/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 15:42:12 -07:00
Jan-Lukas Else a33b1adf13 Add description field to feed settings
This adds a new "description" field to the feed settings. This allows to
save custom description regarding a feed. It is also exported and
imported as "description" in OPML.
2024-05-06 15:40:36 -07:00
fin444 a631bd527d options: add FETCH_NEBULA_WATCH_TIME 2024-05-02 16:30:01 -07:00
Alpha Chen ca62b0b36b integration/raindrop: initial draft implementation 2024-05-02 16:23:00 -07:00
Kioubit 7d6a4243c1 Make cookie duration dependent on configuration
This ensures that session cookies are not expiring before the session is cleaned up from the database as per CLEANUP_REMOVE_SESSIONS_DAYS.
As of now the usefulness of this configuration option is diminished as extending it has no effect on the actual browser session due to the cookie expiry.
Fixes: #2214
2024-05-01 19:34:13 -07:00
dependabot[bot] d056aa1f73 Bump github.com/PuerkitoBio/goquery from 1.9.1 to 1.9.2
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.9.1...v1.9.2)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-30 17:23:35 -07:00
dependabot[bot] 018e24404e Bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 16:51:20 -07:00
Frédéric Guillot 4d3ee0d15d ci: fix docker workflow to add distroless suffix on latest tag 2024-04-27 15:26:16 -07:00
Frédéric Guillot 797450986b Update ChangeLog 2024-04-27 15:06:28 -07:00
Ztec 93bc9ce24d add seek and speed controls to media player
When listening to podcast, it is usual to want to speed up the playback.
https://github.com/miniflux/v2/pull/2521 was addressing the need globally, this PR
allow to address it for just the current open enclosure media. (no save) Some Browser
already include this control directly, but firefox does not (directly anyway).

Also, it is often useful to be able to skip chunk of a podcast, to skip commercials
for example, or get back a bit because we couldn't hear the last part. I added rudimentary
seek controls with the usual +/-10 and 30 seconds chuck size. This is pretty handy when podcast
are very long and using the seek bar is way too tricky to just skip 30s.

As always, I'm French and could only provide English and French translation for the few
text I added in the locale/translations files. Any help is welcome.

Tested mostly on Firefox (121.0) and quickly on Vivaldi(6.5.3206.53), chrome based.

Fixes: #1845 #1846
2024-04-26 13:44:26 -07:00
dependabot[bot] 9233568da3 Bump github.com/tdewolff/minify/v2 from 2.20.19 to 2.20.20
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.19 to 2.20.20.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.19...v2.20.20)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 19:09:18 -07:00
Frédéric Guillot fb075b60b5 reader/processor: minifier is breaking HTML entry content 2024-04-23 20:31:52 -07:00
Frédéric Guillot 2c4c845cd2 http/response: add brotli compression support 2024-04-19 12:16:49 -07:00
bo0tzz 2caabbe939 fix: Use FORCE_REFRESH_INTERVAL config for category refresh 2024-04-19 11:58:13 -07:00
Frédéric Guillot 771f9d2b5f reader/fetcher: add brotli content encoding support 2024-04-19 10:50:46 -07:00
Romain de Laage 647c66e70a ui: add tag entries page 2024-04-14 20:08:38 -07:00
jvoisin b205b5aad0 reader/processor: minimize the feed's entries html
Compress the html of feed entries before storing it. This should reduce the
size of the database a bit, but more importantly, reduce the amount of data
sent to clients

minify being [stupidly fast](https://github.com/tdewolff/minify/?tab=readme-ov-file#performance), the performance impact should be in the noise level.
2024-04-10 19:48:48 -07:00
goodfirm 4ab0d9422d chore: fix function name in comment
Signed-off-by: goodfirm <fanyishang@yeah.net>
2024-04-10 19:36:30 -07:00
Frédéric Guillot 38b80d96ea storage: change GetReadTime() function to use entries_feed_id_hash_key index 2024-04-09 20:37:30 -07:00
Michael Kuhn 35edd8ea92 Fix clicking unread counter
When clicking the unread counter, the following exception occurs:
```
Uncaught TypeError: Cannot read properties of null (reading 'getAttribute')
```

This is due to `onClickMainMenuListItem` not working correctly for the
unread counter `span`s, which return `null` when using `querySelector`.
2024-04-09 20:36:42 -07:00
Alexandros Kosiaris f0cb041885 Add back removed other repo owners in GH docker actions
In cf96ab45c1, support was added for using Docker related Github
actions in repositories of other owners. This was pretty helpful as it
allowed running modified forks off of main in a nightly fashion before
patches were pushed upstream. This was 6e870cdccc, add it
back
2024-04-06 11:31:29 -07:00
Frédéric Guillot fdd1b3f18e database: entry URLs can exceeds btree index size limit 2024-04-04 20:22:23 -07:00
Frédéric Guillot 6e870cdccc ci: use docker/metadata-action instead of deprecated shell-scripts 2024-04-04 18:04:32 -07:00
Michael Kuhn 194f517be8 Improve Dockerfiles
- Specify Docker registry explicitly (e.g., Podman does not use
  `docker.io` by default)
- Use `make miniflux` instead of duplicating `go build` arguments (this
  leverages Go's PIE build mode)
- Enable cgo to fix ARM containers (we need to make sure to use the same
  OS version for both container stages to avoid libc issues)
2024-04-04 17:36:28 -07:00
dependabot[bot] 11fd1c935e Bump golang.org/x/net from 0.23.0 to 0.24.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/net/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 16:37:41 -07:00
dependabot[bot] 47e1111908 Bump golang.org/x/term from 0.18.0 to 0.19.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/term/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 16:08:16 -07:00
dependabot[bot] c5b812eb7b Bump golang.org/x/oauth2 from 0.18.0 to 0.19.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 15:46:40 -07:00
dependabot[bot] 53be550e8a Bump github.com/yuin/goldmark from 1.7.0 to 1.7.1
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:30:47 -07:00
dependabot[bot] d0d693a6ef Bump golang.org/x/net from 0.22.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:30:26 -07:00
Evan Elias Young 1b8c45d162 finder: Find feed from YouTube playlist
The feed from a YouTube playlist page is derived in practically the same way as a feed from a YouTube channel page.
2024-04-01 21:16:32 -07:00
jvoisin 19ce519836 reader/rewrite: add a rule for oglaf.com
By default, Oglaf show some disclaimer/warning about its content, and this
doesn't play well with rss readers, so let's rewrite it to show the actual
comic instead of a placeholder.
2024-04-01 21:05:01 -07:00
Thomas J Faughnan Jr 3e0d5de7a3 api tests: use intSize-agnostic random integers
rand.Intn(math.MaxInt64) causes tests to fail on 32-bit architectures.
Use the simpler rand.Int() instead, which still provides plenty of room
for generating pseudo-random test usernames.
2024-04-01 21:02:48 -07:00
296 changed files with 23853 additions and 12483 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/go:1.22
image: mcr.microsoft.com/devcontainers/go:1.23
volumes:
- ..:/workspace:cached
command: sleep infinity
-10
View File
@@ -1,10 +0,0 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug, triage needed
assignees: ''
---
+105
View File
@@ -0,0 +1,105 @@
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
+81
View File
@@ -0,0 +1,81 @@
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
-10
View File
@@ -1,10 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
- [ ] I have read this document: https://miniflux.app/opinionated.html#feature-request
@@ -0,0 +1,65 @@
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
-10
View File
@@ -1,10 +0,0 @@
---
name: Feed Problems
about: Problems with a feed or a website
title: ''
labels: feed problems, triage needed
assignees: ''
---
+84
View File
@@ -0,0 +1,84 @@
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
+86
View File
@@ -0,0 +1,86 @@
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
+5 -5
View File
@@ -1,7 +1,7 @@
Do you follow the guidelines?
Have you followed these guidelines?
- [ ] I have tested my changes
- [ ] There is no breaking changes
- [ ] I really tested my changes and there is no regression
- [ ] Ideally, my commit messages use the same convention as the Go project: https://go.dev/doc/contribute#commit_messages
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
- [ ] 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 this document: https://miniflux.app/faq.html#pull-request
+3 -3
View File
@@ -9,13 +9,13 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Golang
uses: actions/setup-go@v5
with:
go-version: "1.22.x"
go-version: "1.24.x"
check-latest: true
- name: Checkout
uses: actions/checkout@v4
- name: Compile binaries
env:
CGO_ENABLED: 0
+9 -3
View File
@@ -5,9 +5,17 @@ permissions: read-all
on:
push:
branches: [ main ]
paths:
- '**.js'
- '**.go'
- '!**_test.go'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
paths:
- '**.js'
- '**.go'
- '!**_test.go'
schedule:
- cron: '45 22 * * 3'
@@ -22,8 +30,6 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
@@ -31,7 +37,7 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: "1.22.x"
go-version: "1.24.x"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
+6 -2
View File
@@ -5,11 +5,15 @@ on:
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.pull_request
if: github.event_name == 'schedule' || github.event_name == 'pull_request'
name: Test Packages
runs-on: ubuntu-latest
steps:
@@ -30,7 +34,7 @@ jobs:
- name: List generated files
run: ls -l *.deb
build-packages-manually:
if: github.event_name != 'pull_request' && github.event_name != 'push'
if: github.event_name == 'workflow_dispatch'
name: Build Packages Manually
runs-on: ubuntu-latest
steps:
+41 -61
View File
@@ -7,36 +7,11 @@ on:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches: [ main ]
paths:
- 'packaging/docker/**'
jobs:
test-docker-images:
if: github.event.pull_request
name: Test Images
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Alpine image
uses: docker/build-push-action@v5
with:
context: .
file: ./packaging/docker/alpine/Dockerfile
push: false
tags: ${{ github.repository_owner }}/miniflux:alpine-dev
- name: Test Alpine Docker image
run: docker run --rm ${{ github.repository_owner }}/miniflux:alpine-dev miniflux -i
- name: Build Distroless image
uses: docker/build-push-action@v5
with:
context: .
file: ./packaging/docker/distroless/Dockerfile
push: false
tags: ${{ github.repository_owner }}/miniflux:distroless-dev
- name: Test Distroless Docker image
run: docker run --rm ${{ github.repository_owner }}/miniflux:distroless-dev miniflux -i
publish-docker-images:
if: ${{ ! github.event.pull_request }}
name: Publish Images
docker-images:
name: Docker Images
permissions:
packages: write
runs-on: ubuntu-latest
@@ -46,33 +21,33 @@ jobs:
with:
fetch-depth: 0
- name: Generate Alpine Docker tag
id: docker_alpine_tag
run: |
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
DOCKER_VERSION=dev
if [ "${{ github.event_name }}" = "schedule" ]; then
DOCKER_VERSION=nightly
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
elif [[ $GITHUB_REF == refs/tags/* ]]; then
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest,quay.io/${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=tags::${TAGS}
- name: Generate Alpine Docker tags
id: docker_alpine_tags
uses: docker/metadata-action@v5
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 tag
id: docker_distroless_tag
run: |
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
DOCKER_VERSION=dev-distroless
if [ "${{ github.event_name }}" = "schedule" ]; then
DOCKER_VERSION=nightly-distroless
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
elif [[ $GITHUB_REF == refs/tags/* ]]; then
DOCKER_VERSION=${GITHUB_REF#refs/tags/}-distroless
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest-distroless,ghcr.io/${DOCKER_IMAGE}:latest-distroless,quay.io/${DOCKER_IMAGE}:latest-distroless"
fi
echo ::set-output name=tags::${TAGS}
- name: Generate Distroless Docker tags
id: docker_distroless_tags
uses: docker/metadata-action@v5
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@v3
@@ -81,12 +56,14 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
uses: docker/login-action@v3
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@v3
with:
registry: ghcr.io
@@ -94,6 +71,7 @@ jobs:
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@v3
with:
registry: quay.io
@@ -101,19 +79,21 @@ jobs:
password: ${{ secrets.QUAY_TOKEN }}
- name: Build and Push Alpine images
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
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: true
tags: ${{ steps.docker_alpine_tag.outputs.tags }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_alpine_tags.outputs.tags }}
- name: Build and Push Distroless images
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
with:
context: .
file: ./packaging/docker/distroless/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker_distroless_tag.outputs.tags }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_distroless_tags.outputs.tags }}
+22 -7
View File
@@ -28,12 +28,27 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22.x"
- run: "go vet ./..."
- uses: golangci/golangci-lint-action@v4
go-version: "1.24.x"
- uses: golangci/golangci-lint-action@v7
with:
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace --enable gocritic
- uses: dominikh/staticcheck-action@v1.3.1
args: >
--timeout 10m
--disable errcheck
--enable sqlclosecheck,misspell,whitespace,gocritic
- 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@v4
with:
version: "2023.1.7"
install-go: false
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
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 }}
+6 -2
View File
@@ -5,11 +5,15 @@ on:
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
jobs:
test-package:
if: github.event.pull_request
if: github.event_name == 'schedule' || github.event_name == 'pull_request'
name: Test Packages
runs-on: ubuntu-latest
steps:
@@ -21,7 +25,7 @@ jobs:
- name: List generated files
run: ls -l *.rpm
build-package-manually:
if: github.event_name != 'pull_request' && github.event_name != 'push'
if: github.event_name == 'workflow_dispatch'
name: Build Packages Manually
runs-on: ubuntu-latest
steps:
@@ -0,0 +1,93 @@
import subprocess
import re
import sys
import argparse
from typing import Match
# Conventional commit pattern
CONVENTIONAL_COMMIT_PATTERN: str = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}"
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()
+11 -7
View File
@@ -15,16 +15,20 @@ jobs:
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
go-version: ["1.22.x"]
go-version: ["1.24.x"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
- name: Run unit tests
- 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
@@ -40,12 +44,12 @@ jobs:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22.x"
- name: Checkout
uses: actions/checkout@v4
go-version: "1.24.x"
- name: Install Postgres client
run: sudo apt update && sudo apt install -y postgresql-client
- name: Run integration tests
+5 -4
View File
@@ -1,6 +1,7 @@
miniflux-*
./*.sha256
./miniflux
*.rpm
*.deb
.idea
.vscode
.vscode
*.deb
*.rpm
miniflux-*
+411
View File
@@ -1,3 +1,414 @@
Version 2.2.8 (April 22, 2025)
------------------------------
* refactor(js): replace `DomHelper` methods with standalone functions
* refactor: avoid logging twice the feed errors in the background worker
* fix(api): `hide_globally` categories field should be a boolean
* fix(ui): add missing `await` when calling `navigator.share()` method
* fix(ui): replace share link with a form button for better accessibility
* feat(telegrambot): replace "Go to website" button with "Go to Miniflux"
* feat(locale): update Polish translation
* feat(locale): update German translation
* feat(locale): update Chinese translation
* feat(config): add `SCHEDULER_ROUND_ROBIN_MAX_INTERVAL` option
* feat(cli): add `-reset-feed-next-check-at` argument
* feat(api): add `update_content` query parameter to `/entries/{entryID}/fetch-content` endpoint
* feat: use `Cache-Control` max-age and `Expires` headers to calculate next check
* feat: implement proxy URL per feed
* feat: add proxy rotation functionality
* ci(linter): replace commitlint with a Python script
* ci: add documentation issue template
* build(deps): bump `golang.org/x/oauth2` from `0.28.0` to `0.29.0`
* build(deps): bump `golang.org/x/net` from `0.38.0` to `0.39.0`
* build(deps): bump `golang.org/x/image` from `0.25.0` to `0.26.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.22.4` to `2.23.1`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.2` to `1.10.3`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/mattn/go-sqlite3` from `1.14.24` to `1.14.28`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.12.2` to `0.12.3`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.13.0` to `3.14.1`
Version 2.2.7 (April 1, 2025)
-----------------------------
* test(api): update base URL after upgrading Hugo
* refactor(rewrite): reorganize referer rules and remove obsolete mappings
* refactor: combine feed icon handlers to use only `externalIconID`
* fix(ui): update share feature to correctly select the title element and handle empty titles
* fix(ui): update entry tags display logic to show links based on user authentication
* fix(ui): remove touch-action style to prevent horizontal scrolling issues
* fix(ui): log a warning for an empty client secret
* fix(ui): change labels from "Read / Unread" to "Mark as Read"
* fix(ui): avoid 500 errors and NaN when marking a deleted entry as read
* fix(subscription): add `/rss/feed.xml` to the list of known feed URLs
* fix(security): use a more restrictive CSP for untrusted content
* fix(rewrite): remove obsolete rule for `webtoons.com`
* fix(processor): add missing quotation marks to import comments
* fix(googlereader): return enclosures in the `streamItemContentsHandler` response
* fix: address minor issues detected by Go linters
* feat(urlcleaner): add more Google Analytics parameters
* feat(storage): reduce the number of SQL queries when fetching entry enclosures
* feat(sanitizer): allow the `<u>` tag in feeds
* feat(sanitizer): allow the `<b>` tag
* feat(locale): update Polish translation
* feat(locale): add Romanian translation
* feat(integrations/ntfy): make ntfy topics configurable per feed
* feat(googlereader): add a feed icon endpoint
* feat: show database size on the about page
* feat: add a `make add string` command to add new localized strings
* docs: update README
* docs: update client README to remove references to deprecated functions
* ci: replace GitHub Issue Markdown templates with YAML forms
* build(deps): bump `golangci/golangci-lint-action` from `6` to `7`
* build(deps): bump `golang.org/x/term` from `0.29.0` to `0.30.0`
* build(deps): bump `golang.org/x/oauth2` from `0.26.0` to` 0.28.0`
* build(deps): bump `golang.org/x/net` from `0.35.0` to `0.38.0`
* build(deps): bump `golang.org/x/image` from `0.24.0` to `0.25.0`
* build(deps): bump `golang.org/x/crypto` from `0.33.0` to `0.36.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.21.3` to `2.22.4`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/golang-jwt/jwt/v5` from `5.2.1` to `5.2.2`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.11.2` to `0.12.2`
* build(deps): bump `github.com/go-jose/go-jose/v4` from `4.0.2` to `4.0.5`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.12.0` to `3.13.0`
Version 2.2.6 (February 22, 2025)
---------------------------------
* test(encoding): add unit tests for `CharsetReader` function
* refactor(xml): improve the performances of `NewXMLDecoder`
* refactor(ui): remove superfluous cast
* refactor(request): broaden an error condition when parsing cookies
* refactor(processor): remove superfluous parenthesis
* refactor(opml): don't define receivers on both values and pointer
* refactor(model): simplify a condition
* refactor(model): don't define methods both on instance and pointer
* refactor(locale): sort JSON documents alphabetically by keys
* refactor(locale): remove superfluous parenthesis
* refactor(js): use proper types in `app.js`
* refactor(js): replace the deprecated `window.pageYOffset` with `window.scollY`
* refactor(js): remove a useless `return`
* refactor(js): anchor `=` removal in `webauthn_handler.js` regex
* refactor(js): add default value for parameter `fallbackSelf` in `goToPage` function
* refactor(integration): don't use `defer` in a loop
* refactor(icon): guard against a potential `null` dereference
* refactor(date): use an else-if instead of two if statements
* refactor(css): use shortcuts to declare padding
* refactor(client): remove a useless cast
* perf(sanitizer): remove two useless calls to `strings.ReplaceAll`
* fix(ui): Redirect correctly post feed removal from category feeds list
* fix(scraper): update TechCrunch scraper rule
* fix(scraper): avoid encoding issue if charset meta tag is after 1024 bytes
* fix(sanitizer): non-allowed attributes are not properly stripped
* fix(sanitizer): correct HTML tag name from `tfooter` to `tfoot`
* fix(rss): handle item title with `CDATA` content correctly
* fix(locale): missing hyphen in `de_DE.json`
* fix(css): avoid aside overflow on the pagination menu
* fix(css): `--entry-content-aside-border-color` is missing from `system.css`
* fix(api): return 500 response when JSON serialization fails
* fix(api): JSON encoding is failing with dates at OAD and negative timezone offset
* feat(urlcleaner): add trackers to the blocklist
* feat(ui): open the `<details>` tag in edit feed page when the feature is enabled
* feat(sanitizer): improve text truncation with better space handling
* feat(sanitizer): allow `img` tags with only a `srcset` and no `src` attribute
* feat(rss): add workaround for RSS item title with HTML content
* feat(pushover): add integration with pushover.net
* feat(processor): fetch YouTube watch time in bulk using the API
* feat(locale): update Traditional Chinese translation
* feat(locale): update Polish translation
* feat(locale): update French translation
* feat(locale): add Taiwanese POJ (nan-Latn-pehoeji)
* feat(integration): update Linkace integration to support API v2
* feat(integration): add webhook URL per feed
* feat(integration): add Slack integration
* feat(css): improve aside element position on smartphone
* ci: update GitHub Actions workflows to use Go 1.24
* ci: trigger packaging tests on pull requests
* ci: add `commitlint` to validate PR commit messages
* build(deps): bump `golang` in `/packaging/debian`
* build(deps): bump `golang.org/x/term` from `0.28.0` to `0.29.0`
* build(deps): bump `golang.org/x/oauth2` from `0.25.0` to `0.26.0`
* build(deps): bump `golang.org/x/net` from `0.34.0` to `0.35.0`
* build(deps): bump `golang.org/x/image` from `0.23.0` to `0.24.0`
* build(deps): bump `golang.org/x/crypto` from `0.32.0` to `0.33.0`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.1` to `1.10.2`
Version 2.2.5 (January 20, 2025)
--------------------------------
* test(js): improve `.jshintrc` (strict comparison, etc...)
* test(sanitizer): add a fuzzer
* refactor(rewriter): use custom title case converter implementation instead of `golang.org/x/text/cases.Title()`
* refactor(readingtime): replace `whatlanggo` package with an ad-hoc implementation
* refactor(oauth2): no need to use `io.WriteString` when sha256 provides a way to obtain a sum in a single call
* refactor(js): simplify a bit `keyboard_handler.js`
* refactor(js): remove an outdated check for `{passive: true}`
* refactor(js): minor refactoring of `touch_handler.js`
* refactor(js): minor improvements in `app.js`
* refactor(database): add special handling for PostgreSQL-specific migrations
* fix(ui): reading preferences are reset if the form values are incorrect
* fix(sanitizer): allow `<hr>` tags
* fix(finder): do not add redirections to the list of subscriptions to avoid confusion
* fix: update Wallabag URL label to avoid confusion
* fix: improve pagination when having identical publication date
* fix: do not strip tags in Atom entry title
* feat(ntfy): Add option to use internal links
* feat(locale): update Polish translation
* feat(locale): update German translation
* feat(integration): add Discord integration
* feat(database): add optional build support for SQLite
* feat: validate usernames upon creation
* feat: replace `%{?systemd_requires}` with `%{?systemd_ordering}`
* feat: bump linter and minifier from ECMAScript 2017 to 2020 (ES11)
* feat: add `fix_ghost_cards` rewrite rule
* ci: tighten the CodeQL rules
* ci: run Docker tests only when the Dockerfiles are modified
* ci: run `-race -cover` only on Ubuntu jobs
* ci: don't specify languages for CodeQL
* ci: don't run `go vet ./...` as it's run as part of `golangci-lint`
* ci: checkout before installing Go to improve cache efficiency
* ci: avoid building Linux packages for each pull-request
* build(deps): bump `golang.org/x/oauth2` from `0.24.0` to `0.25.0`
* build(deps): bump `golang.org/x/net` from `0.33.0` to `0.34.0`
* build(deps): bump `golang.org/x/crypto` from `0.31.0` to `0.32.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.21.2` to `2.21.3`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.0` to `1.10.1`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.11.0` to `3.12.0`
Version 2.2.4 (December 20, 2024)
---------------------------------
* test(rewrite): add unit test for referer rewrite function
* refactor(subscription): use `strings.HasSuffix` instead of a regex in `FindSubscriptionsFromYouTubePlaylistPage`
* refactor(sanitizer): use `token.String()` instead of `html.EscapeString(token.Data)`
* refactor(sanitizer): simplify `isValidTag`
* refactor(sanitizer): simplify `hasRequiredAttributes`
* refactor(sanitizer): remove condition because `config.Opts` is guaranteed to never be nil
* refactor(sanitizer): remove a now-useless function after refactoring
* refactor(sanitizer): refactor conditions to highlight their similitude, enabling further refactoring
* refactor(sanitizer): optimize `strip_tags.go`
* refactor(sanitizer): micro-optimizations of `srcset.go`
* refactor(sanitizer): merge two conditions
* refactor(sanitizer): inline a function in `sanitizeAttributes` and fix a bug in it
* refactor(sanitizer): inline a condition in `sanitizeSrcsetAttr`
* refactor(sanitizer): improve `rewriteIframeURL()`
* refactor(sanitizer): Google+ isn't a thing anymore
* refactor(sanitizer): change the scope of a variable
* refactor(rewriter): replace regex with URL parsing for referrer override
* refactor(rewriter): avoid the use of regex in `addDynamicImage`
* refactor(rewrite): remove unused function arguments
* refactor(readability): various improvements and optimizations
* refactor(readability): simplify the regexes in `readability.go`
* refactor(processor): use URL parsing instead of a regex
* refactor(processor): improve the `rewrite` URL rule regex
* refactor(locale): delay parsing of translations until they're used
* refactor(js): factorise a line in `app.js`
* refactor(handler): delay `store.UserByID()` as much as possible
* refactor(css): replace `-ms-text-size-adjust` with `text-size-adjust`
* refactor(css): remove `-webkit-clip-path`
* refactor(css): factorise `.pagination-next` and `.pagination-last` together
* refactor: use a better construct than `doc.Find(…).First()`
* refactor: use `min/max` instead of `math.Min/math.Max`
* refactor: refactor `internal/reader/readability/testdata`
* refactor: optimize `sanitizeAttributes`
* refactor: get rid of `numberOfPluralFormsPerLanguage` test-only variable
* fix(storage): replace timezone function call with view
* fix(consistency): align feed modification behavior between API and UI
* fix(ci): fix grammar in pull-request template
* fix: load icon from site URL instead of feed URL
* fix: feed icon from xml ignored during force refresh
* feat(rewrite)!: remove `parse_markdown` rewrite rule
* feat(mediaproxy): update predefined referer spoofing rules for restricted media resources
* feat(locale): update translations to clarify readeck URL instead of readeck API endpoint
* feat(locale): update German translations
* feat(locale): update Chinese translations
* feat(apprise): update `SendNotification` to handle multiple entries and add logging
* feat(apprise): add title in notification request body
* feat: resize favicons before storing them in the database
* feat: optionally fetch watch time from YouTube API instead of website
* feat: only show the commit URL if it's not empty on `/about`
* feat: add predefined scraper rules for `arstechnica.com`
* feat: add date-based entry filtering rules
* chore: remove `blog.laravel.com` rewrite rule
* build(deps): bump `library/alpine` in `/packaging/docker/alpine` to `3.21`
* build(deps): bump `golang.org/x/term` from `0.26.0` to `0.27.0`
* build(deps): bump `golang.org/x/net` from `0.31.0` to `0.33.0`
* build(deps): bump `golang.org/x/crypto` from `0.30.0` to `0.31.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.21.1` to `2.21.2`
Version 2.2.3 (November 10, 2024)
---------------------------------
* fix: unable to change password due to a typo in SQL parameter
* fix: show only one player when there are several audio/video enclosures
* feat(mediaproxy): pass original filename in `Content-Disposition` header
* feat(mediaproxy): implement referer spoofing for restricted media resources
* feat(integration): update Shiori integration to use new API endpoints for login/bookmark
* build(deps): bump `golang.org/x/text` from `0.19.0` to `0.20.0`
* build(deps): bump `golang.org/x/term` from `0.25.0` to `0.26.0`
* build(deps): bump `golang.org/x/oauth2` from `0.23.0` to `0.24.0`
* build(deps): bump `golang.org/x/net` from `0.30.0` to `0.31.0`
* build(deps): bump `golang.org/x/crypto` from `0.28.0` to `0.29.0`
Version 2.2.2 (October 29, 2024)
--------------------------------
* fix(webauthn): add backup eligibility flag workaround to avoid a 401 response
* fix: update `Last-Modified` if it changes in a 304 response
* feat(webauthn): show help message regarding username and non-discoverable credentials
* feat(rss): calculate hash based on item title/content for feeds without GUID and link
* feat(locale): update Chinese translations
* feat(locale): update Polish translations
* feat(integration): add Cubox integration
* feat(client): add `custom_js` field to Go API client
* feat(api): add endpoint for user integration status
* feat: update feed icon during force refresh
* feat: take `Retry-After` header into consideration for rate limited feeds
* feat: set entry URL to rewritten URL if a rewrite rule is defined
* feat: replace `xurls` third-party module with an ad-hoc regexp
* feat: add new settings option to allow external fonts
* feat: add custom user JavaScript similar to custom CSS
* chore: update test case comment
* build(deps): bump `golang.org/x/net` from `0.29.0` to `0.30.0`
* build(deps): bump `github.com/yuin/goldmark` from `1.7.4` to `1.7.8`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.37` to `2.21.1`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/andybalholm/brotli` from `1.1.0` to `1.1.1`
Version 2.2.1 (September 28, 2024)
----------------------------------
* refactor: split processor package into smaller files
* fix(mediaproxy): forward client user-agent to origin to bypass bot protection
* fix: use root URL to generate absolute proxy URL
* fix: remove progression save on shared entry
* fix: add datasource variable and upgrade depecrated panels on the Grafana dashboard
* feat(locale): update zh_CN translations
* feat(locale): update Ukrainian translations
* feat(locale): update Spanish translations
* feat(locale): update Dutch translations
* feat: use Bilibili API instead of web scraping to get videos watch time
* feat: add pagination to shared entries listing
* feat: add button to show only starred entries per category
* build(deps): bump `golang.org/x/term` from `0.23.0` to `0.24.0`
* build(deps): bump `golang.org/x/oauth2` from `0.22.0` to `0.23.0`
* build(deps): bump `golang.org/x/net` from `0.28.0` to `0.29.0`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.9.2` to `1.10.0`
* build(deps): bump `github.com/prometheus/client_golang` from `1.20.3` to `1.20.4`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.10.2` to `0.11.2`
* build: update go.mod to Go 1.23
* build: bump devcontainer version to go 1.23
Version 2.2.0 (August 18, 2024)
-------------------------------
* refactor: simplify Youtube feeds discovery
* fix(integration): define content encoding explicitly when sending article body to Readeck
* fix(fever): correct sorting direction when using `max_id` argument
* fix(client): Return `nil` and error if endpoint is an empty string
* fix: video poster image URL is encoded twice when using `MEDIA_PROXY_MODE=all`
* fix: use `BASE_URL` instead of `r.Host` to generate absolute media proxy URL
* fix: panic during YouTube channel feed discovery
* fix: honor `hide_globally` when creating a new feed through the api
* fix: align pagination correctly on small screens with non-English text
* fix: `store.GetEnclosure()` should return `nil` if no rows are returned
* feat(locale): update Turkish translations
* feat(locale): update French translations
* feat(locale): update Chinese` translations
* feat(integration): add ntfy integration
* feat(api): add API routes `/v1/enclosures/{enclosureID}`
* feat: validate `OAUTH2_PROVIDER` config option value
* feat: remove YouTube video page subscription finder because `meta[itemprop="channelId"]` no longer exists
* feat: remove well-known URL parameter trackers
* feat: mark media as read when playback reaches 90%
* feat: change log level to info when running migrations
* feat: allow customizing the display name of the OpenID Connect provider
* feat: add support for `base` HTML element when discovering feeds
* feat: add support for `aside` HTML element in entry content
* feat: Add option to disable local auth form
* feat: add license info to Javascript files for LibreJS compatibility
* feat: add `FETCH_BILIBILI_WATCH_TIME` config option
* docs: update links to filtering rules
* chore: avoid using legacy key/value format in Dockerfile
* build(deps): bump `golang.org/x/oauth2` from `0.21.0` to `0.22.0`
* build(deps): bump `golang.org/x/net` from `0.27.0` to `0.28.0`
* build(deps): bump `golang.org/x/crypto` from `0.25.0` to `0.26.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.36` to `2.20.37`
* build(deps): bump `github.com/prometheus/client_golang`
* build: update GitHub Actions to Go 1.23
* build: publish OCI images only if `PUBLISH_DOCKER_IMAGES=true`
* build: bump Alpine Linux build image to v3.20
* build: add sha256 checksum file for published binaries
Version 2.1.4 (July 9, 2024)
----------------------------
* test: add unit tests for `IsModified()` behaviour
* refactor: improve YouTube page feed detection
* fix(ui): settings form is not populated correctly after validation errors
* fix(ui): playback speed indicator precision
* fix(ui): playback speed indicator on shared entries
* fix(integration): preserve existing Pinboard bookmarks
* fix(googlereader): set `CrawlTimeMsec` to the correct precision
* fix(build): failed to solve container image `arm64v8/golang:1.22-bookworm`
* fix(build): add `distroless` suffix on `latest` tag in GitHub workflow
* fix: use `ETag` as a stronger validator than `Last-Modified`
* fix: update `theverge.com` rewrite rule to avoid duplicate image
* fix: incorrect Go package comment `reader/readingtime`
* fix: error out for improper rewrite regexp when processing feed entries
* fix: ensures that session cookies are not expiring before the session is cleaned up from the database as per `CLEANUP_REMOVE_SESSIONS_DAYS`
* fix: `<img>` aspect ratio with `height: auto`
* feat(ui): add `viewport-fit=cover`
* feat(sanitizer): add support for HTML hidden attribute
* feat(locale): update French translations
* feat(integration): add Raindrop integration
* feat(integration): add feed name to Telegram message
* feat(integration): add Betula integration
* feat: use of insecure TLS ciphers when "Allow self-signed or invalid certificates" is enabled to workaround some broken websites
* feat: discover feeds from a Youtube playlist pages
* feat: add navigation to last/first page
* feat: add global block and keep filters
* feat: add description field to feed settings
* feat: add `pitchfork.com` scraping rule
* feat: add `FETCH_NEBULA_WATCH_TIME` config option
* Bump `github.com/PuerkitoBio/goquery` from` 1.9.1` to` 1.9.2`
* Bump `github.com/prometheus/client_golang` from `1.19.0` to `1.19.1`
* build(deps): bump `library/alpine` in `/packaging/docker/alpine`
* build(deps): bump `golangci/golangci-lint-action` from `4` to `6`
* build(deps): bump `golang.org/x/term` from `0.19.0` to `0.22.0`
* build(deps): bump `golang.org/x/oauth2` from `0.19.0` to `0.21.0`
* build(deps): bump `golang.org/x/net` from `0.22.0` to `0.27.0`
* build(deps): bump `golang.org/x/crypto` from `0.24.0` to `0.25.0`
* build(deps): bump `github.com/yuin/goldmark` from `1.7.1` to `1.7.4`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.20` to `2.20.36`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.10.0` to `3.11.0`
* build(deps): bump `docker/build-push-action` from `5` to `6`
Version 2.1.3 (April 27, 2024)
------------------------------
* `api`: `rand.Intn(math.MaxInt64)` causes tests to fail on 32-bit architectures (use `rand.Int()` instead)
* `ci`: use `docker/metadata-action` instead of deprecated shell-scripts
* `database`: remove `entries_feed_url_idx` index because entry URLs can exceeds btree index size limit
* `finder`: find feeds from YouTube playlist
* `http/response`: add brotli compression support
* `integration/matrix`: fix function name in comment
* `packaging`: specify container registry explicitly (e.g., Podman does not use `docker.io` by default)
* `packaging`: use `make miniflux` instead of duplicating `go build` arguments (this leverages Go's PIE build mode)
* `reader/fetcher`: add brotli content encoding support
* `reader/processor`: minimize feed entries HTML content
* `reader/rewrite`: add a rule for `oglaf.com`
* `storage`: change `GetReadTime()` function to use `entries_feed_id_hash_key` index
* `ui`: add seek and speed controls to media player
* `ui`: add tag entries page
* `ui`: fix JavaScript error when clicking on unread counter
* `ui`: use `FORCE_REFRESH_INTERVAL` config for category refresh
* Bump `github.com/tdewolff/minify/v2` from `2.20.19` to `2.20.20`
* Bump `golang.org/x/net` from `0.22.0` to `0.24.0`
* Bump `golang.org/x/term` from `0.18.0` to `0.19.0`
* Bump `golang.org/x/oauth2` from `0.18.0` to `0.19.0`
* Bump `github.com/yuin/goldmark` from `1.7.0` to `1.7.1`
Version 2.1.2 (March 30, 2024)
------------------------------
+39 -19
View File
@@ -1,12 +1,12 @@
APP := miniflux
DOCKER_IMAGE := miniflux/miniflux
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
BUILD_DATE := `date +%FT%T%z`
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
DEB_IMG_ARCH := amd64
APP := miniflux
DOCKER_IMAGE := miniflux/miniflux
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
BUILD_DATE := `date +%FT%T%z`
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
DOCKER_PLATFORM := amd64
export PGPASSWORD := postgres
@@ -51,33 +51,43 @@ miniflux-no-pie:
linux-amd64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-arm64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv7:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv6:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
linux-armv5:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
darwin-amd64:
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
darwin-arm64:
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
freebsd-amd64:
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
openbsd-amd64:
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
windows-amd64:
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
@ sha256sum $(APP)-$@.exe > $(APP)-$@.exe.sha256
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
@@ -104,7 +114,17 @@ 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
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
.PHONY: add-string
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 ./...
@@ -124,7 +144,7 @@ integration-test:
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
RUN_MIGRATIONS=1 \
DEBUG=1 \
LOG_LEVEL=debug \
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! nc -z localhost 8080; do sleep 1; done
@@ -163,15 +183,15 @@ rpm: clean
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
debian:
@ docker build --load \
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
@ docker buildx build --load \
--platform linux/$(DOCKER_PLATFORM) \
-t miniflux-deb-builder \
-f packaging/debian/Dockerfile \
.
@ docker run --rm \
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
@ docker run --rm --platform linux/$(DOCKER_PLATFORM) \
-v ${PWD}:/pkg miniflux-deb-builder
debian-packages: clean
$(MAKE) debian DEB_IMG_ARCH=amd64
$(MAKE) debian DEB_IMG_ARCH=arm64v8
$(MAKE) debian DEB_IMG_ARCH=arm32v7
$(MAKE) debian DOCKER_PLATFORM=amd64
$(MAKE) debian DOCKER_PLATFORM=arm64
$(MAKE) debian DOCKER_PLATFORM=arm/v7
+101 -11
View File
@@ -1,20 +1,110 @@
Miniflux 2
==========
Miniflux is a minimalist and opinionated feed reader:
- Written in Go (Golang)
- Works only with Postgresql
- Doesn't use any ORM
- Doesn't use any complicated framework
- Use only modern vanilla Javascript (ES6 and Fetch API)
- Single binary compiled statically without dependency
- The number of features is voluntarily limited
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
- 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.
### 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), [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/), [Pocket](https://getpocket.com/), [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.
- Sanitizes external content before rendering it.
- Enforces a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that permits only application JavaScript and blocks inline scripts and styles.
- 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.
Documentation
-------------
@@ -29,7 +119,7 @@ The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man
- [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/services.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)
+2 -2
View File
@@ -27,10 +27,10 @@ import (
func main() {
// Authentication with username/password:
client := miniflux.New("https://api.example.org", "admin", "secret")
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
// Authentication with an API Key:
client := miniflux.New("https://api.example.org", "my-secret-token")
client := miniflux.NewClient("https://api.example.org", "my-secret-token")
// Fetch all feeds.
feeds, err := client.Feeds()
+81 -4
View File
@@ -18,6 +18,7 @@ type Client struct {
}
// New returns a new Miniflux client.
//
// Deprecated: use NewClient instead.
func New(endpoint string, credentials ...string) *Client {
return NewClient(endpoint, credentials...)
@@ -185,6 +186,25 @@ func (c *Client) MarkAllAsRead(userID int64) error {
return err
}
// IntegrationsStatus fetches the integrations status for the logged user.
func (c *Client) IntegrationsStatus() (bool, error) {
body, err := c.request.Get("/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 try to find subscriptions from a website.
func (c *Client) Discover(url string) (Subscriptions, error) {
body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
@@ -219,8 +239,8 @@ func (c *Client) Categories() (Categories, error) {
// CreateCategory creates a new category.
func (c *Client) CreateCategory(title string) (*Category, error) {
body, err := c.request.Post("/v1/categories", map[string]interface{}{
"title": title,
body, err := c.request.Post("/v1/categories", &CategoryCreationRequest{
Title: title,
})
if err != nil {
return nil, err
@@ -235,10 +255,25 @@ func (c *Client) CreateCategory(title string) (*Category, error) {
return category, nil
}
// CreateCategoryWithOptions creates a new category with options.
func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCreationRequest) (*Category, error) {
body, err := c.request.Post("/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) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
"title": title,
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), &CategoryModificationRequest{
Title: SetOptionalField(title),
})
if err != nil {
return nil, err
@@ -253,6 +288,22 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
return category, nil
}
// UpdateCategoryWithOptions updates a category with options.
func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
body, err := c.request.Put(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 {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
@@ -613,6 +664,28 @@ func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
return feedIcon, nil
}
// Enclosure fetches a specific enclosure.
func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) {
body, err := c.request.Get(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 {
_, err := c.request.Put(fmt.Sprintf("/v1/enclosures/%d", enclosureID), enclosureUpdate)
return err
}
func buildFilterQueryString(path string, filter *Filter) string {
if filter != nil {
values := url.Values{}
@@ -685,6 +758,10 @@ func buildFilterQueryString(path string, filter *Filter) string {
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)
}
+41 -9
View File
@@ -27,6 +27,7 @@ type User struct {
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"`
@@ -42,6 +43,9 @@ type User struct {
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"`
}
func (u User) String() string {
@@ -68,6 +72,7 @@ type UserModificationRequest struct {
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"`
@@ -82,6 +87,9 @@ type UserModificationRequest struct {
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"`
}
// Users represents a list of users.
@@ -89,9 +97,12 @@ type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"user_id,omitempty"`
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 {
@@ -101,6 +112,18 @@ func (c Category) String() string {
// 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"`
@@ -143,6 +166,7 @@ type Feed struct {
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.
@@ -164,6 +188,7 @@ type FeedCreationRequest struct {
KeeplistRules string `json:"keeplist_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedModificationRequest represents the request to update a feed.
@@ -187,6 +212,7 @@ type FeedModificationRequest struct {
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.
@@ -238,12 +264,17 @@ 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"`
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.
@@ -274,6 +305,7 @@ type Filter struct {
CategoryID int64
FeedID int64
Statuses []string
GloballyVisible bool
}
// EntryResultSet represents the response when fetching entries.
+5 -1
View File
@@ -27,6 +27,7 @@ var (
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 {
@@ -62,6 +63,9 @@ func (r *request) Delete(path string) error {
}
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
if r.endpoint == "" {
return nil, ErrEmptyEndpoint
}
if r.endpoint[len(r.endpoint)-1:] == "/" {
r.endpoint = r.endpoint[:len(r.endpoint)-1]
}
@@ -141,7 +145,7 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
func (r *request) buildClient() http.Client {
return http.Client{
Timeout: time.Duration(defaultTimeout * time.Second),
Timeout: defaultTimeout * time.Second,
}
}
File diff suppressed because it is too large Load Diff
+31 -30
View File
@@ -1,48 +1,49 @@
module miniflux.app/v2
// +heroku goVersion go1.22
// +heroku goVersion go1.23
require (
github.com/PuerkitoBio/goquery v1.9.1
github.com/abadojack/whatlanggo v1.0.1
github.com/coreos/go-oidc/v3 v3.10.0
github.com/go-webauthn/webauthn v0.10.2
github.com/PuerkitoBio/goquery v1.10.3
github.com/andybalholm/brotli v1.1.1
github.com/coreos/go-oidc/v3 v3.14.1
github.com/go-webauthn/webauthn v0.12.3
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.19.0
github.com/tdewolff/minify/v2 v2.20.19
github.com/yuin/goldmark v1.7.0
golang.org/x/crypto v0.21.0
golang.org/x/net v0.22.0
golang.org/x/oauth2 v0.18.0
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
mvdan.cc/xurls/v2 v2.5.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/prometheus/client_golang v1.22.0
github.com/tdewolff/minify/v2 v2.23.1
golang.org/x/crypto v0.37.0
golang.org/x/image v0.26.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.29.0
golang.org/x/term v0.31.0
)
require (
github.com/go-webauthn/x v0.1.9 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/go-webauthn/x v0.1.20 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-tpm v0.9.3 // indirect
)
require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/tdewolff/parse/v2 v2.7.12 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)
go 1.22
go 1.23.0
toolchain go1.24.1
+98 -71
View File
@@ -1,122 +1,149 @@
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.23.1 h1:r6sKQrumHzskWZRdhiRa+pZhn7CdBMojACNP9fuKpXQ=
github.com/tdewolff/minify/v2 v2.23.1/go.mod h1:RkUGjklq6uIsBoOdzY3ll35HKKQ2aFqLQhnanBHhDyU=
github.com/tdewolff/parse/v2 v2.7.23 h1:sCW2PNTCM1yVldh5YK/8wrpRI9rSbloUZWjAydlN2IA=
github.com/tdewolff/parse/v2 v2.7.23/go.mod h1:I7TXO37t3aSG9SlPUBefAhgIF8nt7yYUwVGgETIoBcA=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
+3
View File
@@ -72,6 +72,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
sr.HandleFunc("/enclosures/{enclosureID}", handler.getEnclosureByID).Methods(http.MethodGet)
sr.HandleFunc("/enclosures/{enclosureID}", handler.updateEnclosureByID).Methods(http.MethodPut)
sr.HandleFunc("/integrations/status", handler.getIntegrationsStatus).Methods(http.MethodGet)
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
}
+418 -6
View File
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io"
"math"
"math/rand"
"os"
"strings"
@@ -49,7 +48,7 @@ func newIntegrationTestConfig() *integrationTestConfig {
testFeedURL: getDefaultEnvValues("TEST_MINIFLUX_FEED_URL", "https://miniflux.app/feed.xml"),
testFeedTitle: getDefaultEnvValues("TEST_MINIFLUX_FEED_TITLE", "Miniflux"),
testSubscriptionTitle: getDefaultEnvValues("TEST_MINIFLUX_SUBSCRIPTION_TITLE", "Miniflux Releases"),
testWebsiteURL: getDefaultEnvValues("TEST_MINIFLUX_WEBSITE_URL", "https://miniflux.app"),
testWebsiteURL: getDefaultEnvValues("TEST_MINIFLUX_WEBSITE_URL", "https://miniflux.app/"),
}
}
@@ -58,7 +57,7 @@ func (c *integrationTestConfig) isConfigured() bool {
}
func (c *integrationTestConfig) genRandomUsername() string {
return fmt.Sprintf("%s_%10d", c.testRegularUsername, rand.Intn(math.MaxInt64))
return fmt.Sprintf("%s_%10d", c.testRegularUsername, rand.Int())
}
func TestIncorrectEndpoint(t *testing.T) {
@@ -68,10 +67,14 @@ func TestIncorrectEndpoint(t *testing.T) {
}
client := miniflux.NewClient("incorrect url")
_, err := client.Users()
if err == nil {
if _, err := client.Users(); err == nil {
t.Fatal(`Using an incorrect URL should raise an error`)
}
client = miniflux.NewClient("")
if _, err := client.Users(); err == nil {
t.Fatal(`Using an empty URL should raise an error`)
}
}
func TestHealthcheckEndpoint(t *testing.T) {
@@ -589,6 +592,88 @@ func TestUpdateUserEndpointByChangingDefaultTheme(t *testing.T) {
}
}
func TestUpdateUserEndpointByChangingExternalFonts(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
userUpdateRequest := &miniflux.UserModificationRequest{
ExternalFontHosts: miniflux.SetOptionalField(" fonts.example.org "),
}
updatedUser, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest)
if err != nil {
t.Fatal(err)
}
if updatedUser.ExternalFontHosts != "fonts.example.org" {
t.Fatalf(`Invalid external font hosts, got "%v"`, updatedUser.ExternalFontHosts)
}
}
func TestUpdateUserEndpointByChangingExternalFontsWithInvalidValue(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
userUpdateRequest := &miniflux.UserModificationRequest{
ExternalFontHosts: miniflux.SetOptionalField("'self' *"),
}
if _, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest); err == nil {
t.Fatal(`Updating the user with an invalid external font host should raise an error`)
}
}
func TestUpdateUserEndpointByChangingCustomJS(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
userUpdateRequest := &miniflux.UserModificationRequest{
CustomJS: miniflux.SetOptionalField("alert('Hello, World!');"),
}
updatedUser, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest)
if err != nil {
t.Fatal(err)
}
if updatedUser.CustomJS != "alert('Hello, World!');" {
t.Fatalf(`Invalid custom JS, got %q`, updatedUser.CustomJS)
}
}
func TestUpdateUserEndpointByChangingDefaultThemeToInvalidValue(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
@@ -739,6 +824,10 @@ func TestCreateCategoryEndpoint(t *testing.T) {
if category.Title != categoryName {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, category.Title, categoryName)
}
if category.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
}
func TestCreateCategoryWithEmptyTitle(t *testing.T) {
@@ -780,7 +869,49 @@ func TestCannotCreateDuplicatedCategory(t *testing.T) {
}
}
func TestUpdateCatgoryEndpoint(t *testing.T) {
func TestCreateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
HideGlobally: true,
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
categories, err := regularUserClient.Categories()
if err != nil {
t.Fatal(err)
}
for _, category := range categories {
if category.ID == newCategory.ID {
if category.Title != newCategory.Title {
t.Errorf(`Invalid title, got %q instead of %q`, category.Title, newCategory.Title)
}
if category.HideGlobally != true {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
break
}
}
}
func TestUpdateCategoryEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
@@ -818,6 +949,91 @@ func TestUpdateCatgoryEndpoint(t *testing.T) {
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
updatedCategory, err := regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
Title: miniflux.SetOptionalField("new title"),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(true),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if !updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(false),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got %q`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got %q instead of %q`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateInexistingCategory(t *testing.T) {
@@ -1987,6 +2203,176 @@ func TestGetAllEntriesEndpointWithFilter(t *testing.T) {
}
}
func TestGetGlobalEntriesEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
HideGlobally: true,
})
if err != nil {
t.Fatal(err)
}
feedIDEntry, err := regularUserClient.Feed(feedID)
if err != nil {
t.Fatal(err)
}
if feedIDEntry.HideGlobally != true {
t.Fatalf(`Expected feed to have globally_hidden set to true, was false.`)
}
/* Not filtering on GloballyVisible should return all entries */
feedEntries, err := regularUserClient.Entries(&miniflux.Filter{FeedID: feedID})
if err != nil {
t.Fatal(err)
}
if len(feedEntries.Entries) == 0 {
t.Fatalf(`Expected entries but response contained none.`)
}
/* Feed is hidden globally, so this should be empty */
globallyVisibleEntries, err := regularUserClient.Entries(&miniflux.Filter{GloballyVisible: true})
if err != nil {
t.Fatal(err)
}
if len(globallyVisibleEntries.Entries) != 0 {
t.Fatalf(`Expected no entries, got %d`, len(globallyVisibleEntries.Entries))
}
}
func TestUpdateEnclosureEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
})
if err != nil {
t.Fatal(err)
}
result, err := regularUserClient.FeedEntries(feedID, nil)
if err != nil {
t.Fatalf(`Failed to get entries: %v`, err)
}
var enclosure *miniflux.Enclosure
for _, entry := range result.Entries {
if len(entry.Enclosures) > 0 {
enclosure = entry.Enclosures[0]
break
}
}
if enclosure == nil {
t.Skip(`Skipping test, missing enclosure in feed.`)
}
err = regularUserClient.UpdateEnclosure(enclosure.ID, &miniflux.EnclosureUpdateRequest{
MediaProgression: 20,
})
if err != nil {
t.Fatal(err)
}
updatedEnclosure, err := regularUserClient.Enclosure(enclosure.ID)
if err != nil {
t.Fatal(err)
}
if updatedEnclosure.MediaProgression != 20 {
t.Fatalf(`Failed to update media_progression, expected %d but got %d`, 20, updatedEnclosure.MediaProgression)
}
}
func TestGetEnclosureEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
})
if err != nil {
t.Fatal(err)
}
result, err := regularUserClient.FeedEntries(feedID, nil)
if err != nil {
t.Fatalf(`Failed to get entries: %v`, err)
}
var expectedEnclosure *miniflux.Enclosure
for _, entry := range result.Entries {
if len(entry.Enclosures) > 0 {
expectedEnclosure = entry.Enclosures[0]
break
}
}
if expectedEnclosure == nil {
t.Skip(`Skipping test, missing enclosure in feed.`)
}
enclosure, err := regularUserClient.Enclosure(expectedEnclosure.ID)
if err != nil {
t.Fatal(err)
}
if enclosure.ID != expectedEnclosure.ID {
t.Fatalf(`Invalid enclosureID, got %d while expecting %d`, enclosure.ID, expectedEnclosure.ID)
}
if _, err = regularUserClient.Enclosure(99999); err == nil {
t.Fatalf(`Fetching an inexisting enclosure should raise an error`)
}
}
func TestGetEntryEndpoints(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
@@ -2228,6 +2614,32 @@ func TestSaveEntryEndpoint(t *testing.T) {
}
}
func TestFetchIntegrationsStatusEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
hasIntegrations, err := regularUserClient.IntegrationsStatus()
if err != nil {
t.Fatalf("Failed to fetch integrations status: %v", err)
}
if hasIntegrations {
t.Fatalf("New user should not have integrations configured")
}
}
func TestFetchContentEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
+10 -10
View File
@@ -19,18 +19,18 @@ import (
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryCreationRequest model.CategoryCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryCreationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
category, err := h.store.CreateCategory(userID, &categoryRequest)
category, err := h.store.CreateCategory(userID, &categoryCreationRequest)
if err != nil {
json.ServerError(w, r, err)
return
@@ -54,20 +54,20 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
return
}
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryModificationRequest model.CategoryModificationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryModificationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package api // import "miniflux.app/v2/internal/api"
import (
json_parser "encoding/json"
"net/http"
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/validator"
)
func (h *handler) getEnclosureByID(w http.ResponseWriter, r *http.Request) {
enclosureID := request.RouteInt64Param(r, "enclosureID")
enclosure, err := h.store.GetEnclosure(enclosureID)
if err != nil {
json.ServerError(w, r, err)
return
}
if enclosure == nil {
json.NotFound(w, r)
return
}
userID := request.UserID(r)
if enclosure.UserID != userID {
json.NotFound(w, r)
return
}
enclosure.ProxifyEnclosureURL(h.router)
json.OK(w, r, enclosure)
}
func (h *handler) updateEnclosureByID(w http.ResponseWriter, r *http.Request) {
enclosureID := request.RouteInt64Param(r, "enclosureID")
var enclosureUpdateRequest model.EnclosureUpdateRequest
if err := json_parser.NewDecoder(r.Body).Decode(&enclosureUpdateRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if err := validator.ValidateEnclosureUpdateRequest(&enclosureUpdateRequest); err != nil {
json.BadRequest(w, r, err)
return
}
enclosure, err := h.store.GetEnclosure(enclosureID)
if err != nil {
json.ServerError(w, r, err)
return
}
if enclosure == nil {
json.NotFound(w, r)
return
}
userID := request.UserID(r)
if enclosure.UserID != userID {
json.NotFound(w, r)
return
}
enclosure.MediaProgression = enclosureUpdateRequest.MediaProgression
if err := h.store.UpdateEnclosure(enclosure); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
+24 -16
View File
@@ -8,10 +8,8 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
"miniflux.app/v2/internal/integration"
@@ -20,7 +18,6 @@ import (
"miniflux.app/v2/internal/reader/processor"
"miniflux.app/v2/internal/reader/readingtime"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/urllib"
"miniflux.app/v2/internal/validator"
)
@@ -36,19 +33,9 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b
return
}
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entry.Content)
proxyOption := config.Opts.MediaProxyMode()
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
for i := range entry.Enclosures {
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
entry.Enclosures[i].URL = mediaproxy.ProxifyAbsoluteURL(h.router, r.Host, entry.Enclosures[i].URL)
break
}
}
}
}
entry.Enclosures.ProxifyEnclosureURL(h.router)
json.OK(w, r, entry)
}
@@ -149,6 +136,15 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
builder.WithLimit(limit)
builder.WithTags(tags)
builder.WithEnclosures()
if request.HasQueryParam(r, "globally_visible") {
globallyVisible := request.QueryBoolParam(r, "globally_visible", true)
if globallyVisible {
builder.WithGloballyVisible()
}
}
configureFilters(builder, r)
entries, err := builder.GetEntries()
@@ -164,7 +160,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
}
for i := range entries {
entries[i].Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entries[i].Content)
entries[i].Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entries[i].Content)
}
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
@@ -335,6 +331,18 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
return
}
shouldUpdateContent := request.QueryBoolParam(r, "update_content", false)
if shouldUpdateContent {
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
json.ServerError(w, r, err)
return
}
json.OK(w, r, map[string]interface{}{"content": mediaproxy.RewriteDocumentWithRelativeProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
return
}
json.OK(w, r, map[string]string{"content": entry.Content})
}
+1
View File
@@ -121,6 +121,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
}
feedModificationRequest.Patch(originalFeed)
originalFeed.ResetErrorCounter()
if err := h.store.UpdateFeed(originalFeed); err != nil {
json.ServerError(w, r, err)
return
+1 -1
View File
@@ -13,7 +13,7 @@ import (
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
if !h.store.HasIcon(feedID) {
if !h.store.HasFeedIcon(feedID) {
json.NotFound(w, r)
return
}
+5 -2
View File
@@ -11,6 +11,7 @@ import (
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/subscription"
"miniflux.app/v2/internal/validator"
@@ -36,11 +37,13 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(subscriptionDiscoveryRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.WithUserAgent(subscriptionDiscoveryRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(subscriptionDiscoveryRequest.Cookie)
requestBuilder.WithUsernameAndPassword(subscriptionDiscoveryRequest.Username, subscriptionDiscoveryRequest.Password)
requestBuilder.UseProxy(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(subscriptionDiscoveryRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(subscriptionDiscoveryRequest.DisableHTTP2)
+33
View File
@@ -7,6 +7,8 @@ import (
json_parser "encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
@@ -82,6 +84,18 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
}
}
cleanEnd := regexp.MustCompile(`(?m)\r\n\s*$`)
if userModificationRequest.BlockFilterEntryRules != nil {
*userModificationRequest.BlockFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.BlockFilterEntryRules, "")
// Clean carriage returns for Windows environments
*userModificationRequest.BlockFilterEntryRules = strings.ReplaceAll(*userModificationRequest.BlockFilterEntryRules, "\r\n", "\n")
}
if userModificationRequest.KeepFilterEntryRules != nil {
*userModificationRequest.KeepFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.KeepFilterEntryRules, "")
// Clean carriage returns for Windows environments
*userModificationRequest.KeepFilterEntryRules = strings.ReplaceAll(*userModificationRequest.KeepFilterEntryRules, "\r\n", "\n")
}
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
@@ -116,6 +130,25 @@ func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
json.NoContent(w, r)
}
func (h *handler) getIntegrationsStatus(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
if _, err := h.store.UserByID(userID); err != nil {
json.NotFound(w, r)
return
}
hasIntegrations := h.store.HasSaveEntry(userID)
response := struct {
HasIntegrations bool `json:"has_integrations"`
}{
HasIntegrations: hasIntegrations,
}
json.OK(w, r, response)
}
func (h *handler) users(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
+69 -35
View File
@@ -4,6 +4,7 @@
package cli // import "miniflux.app/v2/internal/cli"
import (
"errors"
"flag"
"fmt"
"io"
@@ -12,47 +13,49 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/database"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/ui/static"
"miniflux.app/v2/internal/version"
)
const (
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
)
// Parse parses command line arguments.
func Parse() {
var (
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagResetFeedNextCheckAt bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
)
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
@@ -64,6 +67,7 @@ func Parse() {
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
flag.BoolVar(&flagResetFeedNextCheckAt, "reset-feed-next-check-at", false, flagResetNextCheckAtHelp)
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
@@ -88,6 +92,23 @@ func Parse() {
printErrorAndExit(err)
}
if oauth2Provider := config.Opts.OAuth2Provider(); oauth2Provider != "" {
if oauth2Provider != "oidc" && oauth2Provider != "google" {
printErrorAndExit(fmt.Errorf(`unsupported OAuth2 provider: %q (Possible values are "google" or "oidc")`, oauth2Provider))
}
}
if config.Opts.DisableLocalAuth() {
switch {
case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "":
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source"))
case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed():
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled"))
case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed():
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled"))
}
}
if flagConfigDump {
fmt.Print(config.Opts)
return
@@ -135,10 +156,6 @@ func Parse() {
slog.Info("The default value for DATABASE_URL is used")
}
if err := locale.LoadCatalogMessages(); err != nil {
printErrorAndExit(fmt.Errorf("unable to load translations: %v", err))
}
if err := static.CalculateBinaryFileChecksums(); err != nil {
printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err))
}
@@ -176,7 +193,16 @@ func Parse() {
}
if flagResetFeedErrors {
store.ResetFeedErrors()
if err := store.ResetFeedErrors(); err != nil {
printErrorAndExit(err)
}
return
}
if flagResetFeedNextCheckAt {
if err := store.ResetNextCheckAt(); err != nil {
printErrorAndExit(err)
}
return
}
@@ -215,6 +241,14 @@ func Parse() {
createAdminUserFromEnvironmentVariables(store)
}
if config.Opts.HasHTTPClientProxiesConfigured() {
slog.Info("Initializing proxy rotation", slog.Int("proxies_count", len(config.Opts.HTTPClientProxies())))
proxyrotator.ProxyRotatorInstance, err = proxyrotator.NewProxyRotator(config.Opts.HTTPClientProxies())
if err != nil {
printErrorAndExit(fmt.Errorf("unable to initialize proxy rotator: %v", err))
}
}
if flagRefreshFeeds {
refreshFeeds(store)
return
+180
View File
@@ -259,6 +259,29 @@ func TestCustomBaseURLWithTrailingSlash(t *testing.T) {
}
}
func TestCustomBaseURLWithCustomPort(t *testing.T) {
os.Clearenv()
os.Setenv("BASE_URL", "http://example.org:88/folder/")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
if opts.BaseURL() != "http://example.org:88/folder" {
t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
}
if opts.RootURL() != "http://example.org:88" {
t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
}
if opts.BasePath() != "/folder" {
t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
}
}
func TestBaseURLWithoutScheme(t *testing.T) {
os.Clearenv()
os.Setenv("BASE_URL", "example.org/folder/")
@@ -1005,6 +1028,41 @@ func TestSchedulerRoundRobin(t *testing.T) {
}
}
func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultSchedulerRoundRobinMaxInterval
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestSchedulerRoundRobinMaxInterval(t *testing.T) {
os.Clearenv()
os.Setenv("SCHEDULER_ROUND_ROBIN_MAX_INTERVAL", "150")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := 150
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestPollingParsingErrorLimit(t *testing.T) {
os.Clearenv()
os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100")
@@ -2021,6 +2079,42 @@ func TestAuthProxyUserCreationAdmin(t *testing.T) {
}
}
func TestFetchBilibiliWatchTime(t *testing.T) {
os.Clearenv()
os.Setenv("FETCH_BILIBILI_WATCH_TIME", "1")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := true
result := opts.FetchBilibiliWatchTime()
if result != expected {
t.Fatalf(`Unexpected FETCH_BILIBILI_WATCH_TIME value, got %v instead of %v`, result, expected)
}
}
func TestFetchNebulaWatchTime(t *testing.T) {
os.Clearenv()
os.Setenv("FETCH_NEBULA_WATCH_TIME", "1")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := true
result := opts.FetchNebulaWatchTime()
if result != expected {
t.Fatalf(`Unexpected FETCH_NEBULA_WATCH_TIME value, got %v instead of %v`, result, expected)
}
}
func TestFetchOdyseeWatchTime(t *testing.T) {
os.Clearenv()
os.Setenv("FETCH_ODYSEE_WATCH_TIME", "1")
@@ -2057,6 +2151,24 @@ func TestFetchYouTubeWatchTime(t *testing.T) {
}
}
func TestYouTubeApiKey(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_API_KEY", "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000"
result := opts.YouTubeApiKey()
if result != expected {
t.Fatalf(`Unexpected YOUTUBE_API_KEY value, got %v instead of %v`, result, expected)
}
}
func TestYouTubeEmbedUrlOverride(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
@@ -2109,3 +2221,71 @@ func TestParseConfigDumpOutput(t *testing.T) {
t.Fatal(err)
}
}
func TestHTTPClientProxies(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXIES", "http://proxy1.example.com,http://proxy2.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{"http://proxy1.example.com", "http://proxy2.example.com"}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
for i, proxy := range expected {
if result[i] != proxy {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value at index %d, got %q instead of %q`, i, result[i], proxy)
}
}
}
func TestDefaultHTTPClientProxiesValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected default HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
}
func TestHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "http://proxy.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "http://proxy.example.com"
if opts.HTTPClientProxyURL() == nil || opts.HTTPClientProxyURL().String() != expected {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXY value, got %v instead of %v`, opts.HTTPClientProxyURL(), expected)
}
}
func TestInvalidHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "sche|me://invalid-proxy-url")
parser := NewParser()
_, err := parser.ParseEnvironmentVariables()
if err == nil {
t.Fatalf(`Expected error for invalid HTTP_CLIENT_PROXY value, but got none`)
}
}
+108 -12
View File
@@ -5,6 +5,7 @@ package config // import "miniflux.app/v2/internal/config"
import (
"fmt"
"net/url"
"sort"
"strings"
"time"
@@ -36,6 +37,7 @@ const (
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
defaultSchedulerEntryFrequencyFactor = 1
defaultSchedulerRoundRobinMinInterval = 60
defaultSchedulerRoundRobinMaxInterval = 1440
defaultPollingParsingErrorLimit = 3
defaultRunMigrations = false
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
@@ -56,8 +58,11 @@ const (
defaultMediaResourceTypes = "image"
defaultMediaProxyURL = ""
defaultFilterEntryMaxAgeDays = 0
defaultFetchBilibiliWatchTime = false
defaultFetchNebulaWatchTime = false
defaultFetchOdyseeWatchTime = false
defaultFetchYouTubeWatchTime = false
defaultYouTubeApiKey = ""
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
defaultCreateAdmin = false
defaultAdminUsername = ""
@@ -67,7 +72,9 @@ const (
defaultOAuth2ClientSecret = ""
defaultOAuth2RedirectURL = ""
defaultOAuth2OidcDiscoveryEndpoint = ""
defaultOauth2OidcProviderName = "OpenID Connect"
defaultOAuth2Provider = ""
defaultDisableLocalAuth = false
defaultPocketConsumerKey = ""
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15
@@ -131,6 +138,7 @@ type Options struct {
schedulerEntryFrequencyMaxInterval int
schedulerEntryFrequencyFactor int
schedulerRoundRobinMinInterval int
schedulerRoundRobinMaxInterval int
pollingParsingErrorLimit int
workerPoolSize int
createAdmin bool
@@ -140,20 +148,26 @@ type Options struct {
mediaProxyMode string
mediaProxyResourceTypes []string
mediaProxyCustomURL string
fetchBilibiliWatchTime bool
fetchNebulaWatchTime bool
fetchOdyseeWatchTime bool
fetchYouTubeWatchTime bool
filterEntryMaxAgeDays int
youTubeApiKey string
youTubeEmbedUrlOverride string
oauth2UserCreationAllowed bool
oauth2ClientID string
oauth2ClientSecret string
oauth2RedirectURL string
oidcDiscoveryEndpoint string
oidcProviderName string
oauth2Provider string
disableLocalAuth bool
pocketConsumerKey string
httpClientTimeout int
httpClientMaxBodySize int64
httpClientProxy string
httpClientProxyURL *url.URL
httpClientProxies []string
httpClientUserAgent string
httpServerTimeout int
authProxyHeader string
@@ -208,6 +222,7 @@ func NewOptions() *Options {
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor,
schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval,
schedulerRoundRobinMaxInterval: defaultSchedulerRoundRobinMaxInterval,
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
workerPoolSize: defaultWorkerPoolSize,
createAdmin: defaultCreateAdmin,
@@ -216,19 +231,25 @@ func NewOptions() *Options {
mediaProxyResourceTypes: []string{defaultMediaResourceTypes},
mediaProxyCustomURL: defaultMediaProxyURL,
filterEntryMaxAgeDays: defaultFilterEntryMaxAgeDays,
fetchBilibiliWatchTime: defaultFetchBilibiliWatchTime,
fetchNebulaWatchTime: defaultFetchNebulaWatchTime,
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
youTubeApiKey: defaultYouTubeApiKey,
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
oauth2ClientID: defaultOAuth2ClientID,
oauth2ClientSecret: defaultOAuth2ClientSecret,
oauth2RedirectURL: defaultOAuth2RedirectURL,
oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
oidcProviderName: defaultOauth2OidcProviderName,
oauth2Provider: defaultOAuth2Provider,
disableLocalAuth: defaultDisableLocalAuth,
pocketConsumerKey: defaultPocketConsumerKey,
httpClientTimeout: defaultHTTPClientTimeout,
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
httpClientProxy: defaultHTTPClientProxy,
httpClientProxyURL: nil,
httpClientProxies: []string{},
httpClientUserAgent: defaultHTTPClientUserAgent,
httpServerTimeout: defaultHTTPServerTimeout,
authProxyHeader: defaultAuthProxyHeader,
@@ -415,6 +436,10 @@ func (o *Options) SchedulerRoundRobinMinInterval() int {
return o.schedulerRoundRobinMinInterval
}
func (o *Options) SchedulerRoundRobinMaxInterval() int {
return o.schedulerRoundRobinMaxInterval
}
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
func (o *Options) PollingParsingErrorLimit() int {
return o.pollingParsingErrorLimit
@@ -445,11 +470,21 @@ func (o *Options) OIDCDiscoveryEndpoint() string {
return o.oidcDiscoveryEndpoint
}
// OIDCProviderName returns the OAuth2 OIDC provider's display name
func (o *Options) OIDCProviderName() string {
return o.oidcProviderName
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (o *Options) OAuth2Provider() string {
return o.oauth2Provider
}
// DisableLocalAUth returns true if the local user database should not be used to authenticate users
func (o *Options) DisableLocalAuth() bool {
return o.disableLocalAuth
}
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (o *Options) HasHSTS() bool {
return o.hsts
@@ -481,17 +516,34 @@ func (o *Options) FetchYouTubeWatchTime() bool {
return o.fetchYouTubeWatchTime
}
// YouTubeApiKey returns the YouTube API key if defined.
func (o *Options) YouTubeApiKey() string {
return o.youTubeApiKey
}
// YouTubeEmbedUrlOverride returns YouTube URL which will be used for embeds
func (o *Options) YouTubeEmbedUrlOverride() string {
return o.youTubeEmbedUrlOverride
}
// FetchNebulaWatchTime returns true if the Nebula video duration
// should be fetched and used as a reading time.
func (o *Options) FetchNebulaWatchTime() bool {
return o.fetchNebulaWatchTime
}
// FetchOdyseeWatchTime returns true if the Odysee video duration
// should be fetched and used as a reading time.
func (o *Options) FetchOdyseeWatchTime() bool {
return o.fetchOdyseeWatchTime
}
// FetchBilibiliWatchTime returns true if the Bilibili video duration
// should be fetched and used as a reading time.
func (o *Options) FetchBilibiliWatchTime() bool {
return o.fetchBilibiliWatchTime
}
// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) MediaProxyMode() string {
return o.mediaProxyMode
@@ -545,9 +597,24 @@ func (o *Options) HTTPClientMaxBodySize() int64 {
return o.httpClientMaxBodySize
}
// HTTPClientProxy returns the proxy URL for HTTP client.
func (o *Options) HTTPClientProxy() string {
return o.httpClientProxy
// HTTPClientProxyURL returns the client HTTP proxy URL if configured.
func (o *Options) HTTPClientProxyURL() *url.URL {
return o.httpClientProxyURL
}
// HasHTTPClientProxyURLConfigured returns true if the client HTTP proxy URL if configured.
func (o *Options) HasHTTPClientProxyURLConfigured() bool {
return o.httpClientProxyURL != nil
}
// HTTPClientProxies returns the list of proxies.
func (o *Options) HTTPClientProxies() []string {
return o.httpClientProxies
}
// HTTPClientProxiesString returns true if the list of rotating proxies are configured.
func (o *Options) HasHTTPClientProxiesConfigured() bool {
return len(o.httpClientProxies) > 0
}
// HTTPServerTimeout returns the time limit in seconds before the HTTP server cancel the request.
@@ -555,11 +622,6 @@ func (o *Options) HTTPServerTimeout() int {
return o.httpServerTimeout
}
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
func (o *Options) HasHTTPClientProxyConfigured() bool {
return o.httpClientProxy != ""
}
// AuthProxyHeader returns an HTTP header name that contains username for
// authentication using auth proxy.
func (o *Options) AuthProxyHeader() string {
@@ -622,6 +684,33 @@ func (o *Options) FilterEntryMaxAgeDays() int {
// SortedOptions returns options as a list of key value pairs, sorted by keys.
func (o *Options) SortedOptions(redactSecret bool) []*Option {
var clientProxyURLRedacted string
if o.httpClientProxyURL != nil {
if redactSecret {
clientProxyURLRedacted = o.httpClientProxyURL.Redacted()
} else {
clientProxyURLRedacted = o.httpClientProxyURL.String()
}
}
var clientProxyURLsRedacted string
if len(o.httpClientProxies) > 0 {
if redactSecret {
var proxyURLs []string
for range o.httpClientProxies {
proxyURLs = append(proxyURLs, "<redacted>")
}
clientProxyURLsRedacted = strings.Join(proxyURLs, ",")
} else {
clientProxyURLsRedacted = strings.Join(o.httpClientProxies, ",")
}
}
var mediaProxyPrivateKeyValue string
if len(o.mediaProxyPrivateKey) > 0 {
mediaProxyPrivateKeyValue = "<binary-data>"
}
var keyValues = map[string]interface{}{
"ADMIN_PASSWORD": redactSecretValue(o.adminPassword, redactSecret),
"ADMIN_USERNAME": o.adminUsername,
@@ -647,10 +736,13 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
"FILTER_ENTRY_MAX_AGE_DAYS": o.filterEntryMaxAgeDays,
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
"FETCH_NEBULA_WATCH_TIME": o.fetchNebulaWatchTime,
"FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime,
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
"HTTPS": o.HTTPS,
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
"HTTP_CLIENT_PROXY": o.httpClientProxy,
"HTTP_CLIENT_PROXIES": clientProxyURLsRedacted,
"HTTP_CLIENT_PROXY": clientProxyURLRedacted,
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
@@ -672,9 +764,11 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oidcDiscoveryEndpoint,
"OAUTH2_OIDC_PROVIDER_NAME": o.oidcProviderName,
"OAUTH2_PROVIDER": o.oauth2Provider,
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
"DISABLE_LOCAL_AUTH": o.disableLocalAuth,
"POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret),
"POLLING_FREQUENCY": o.pollingFrequency,
"FORCE_REFRESH_INTERVAL": o.forceRefreshInterval,
@@ -683,7 +777,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
"MEDIA_PROXY_MODE": o.mediaProxyMode,
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
"MEDIA_PROXY_PRIVATE_KEY": mediaProxyPrivateKeyValue,
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
"ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations,
@@ -691,10 +785,12 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
"SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor,
"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval,
"SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": o.schedulerRoundRobinMaxInterval,
"SCHEDULER_SERVICE": o.schedulerService,
"SERVER_TIMING_HEADER": o.serverTimingHeader,
"WATCHDOG": o.watchdog,
"WORKER_POOL_SIZE": o.workerPoolSize,
"YOUTUBE_API_KEY": redactSecretValue(o.youTubeApiKey, redactSecret),
"YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride,
"WEBAUTHN": o.webAuthn,
}
+24 -3
View File
@@ -160,6 +160,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
case "PROXY_IMAGES":
@@ -189,11 +191,15 @@ func (p *Parser) parseLines(lines []string) (err error) {
case "PROXY_PRIVATE_KEY":
slog.Warn("The PROXY_PRIVATE_KEY environment variable is deprecated, use MEDIA_PROXY_PRIVATE_KEY instead")
randomKey := make([]byte, 16)
rand.Read(randomKey)
if _, err := rand.Read(randomKey); err != nil {
return fmt.Errorf("config: unable to generate random key: %w", err)
}
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
case "MEDIA_PROXY_PRIVATE_KEY":
randomKey := make([]byte, 16)
rand.Read(randomKey)
if _, err := rand.Read(randomKey); err != nil {
return fmt.Errorf("config: unable to generate random key: %w", err)
}
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
case "MEDIA_PROXY_CUSTOM_URL":
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
@@ -225,14 +231,23 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
case "OAUTH2_OIDC_PROVIDER_NAME":
p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName)
case "OAUTH2_PROVIDER":
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
case "DISABLE_LOCAL_AUTH":
p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
case "HTTP_CLIENT_TIMEOUT":
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
case "HTTP_CLIENT_MAX_BODY_SIZE":
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
case "HTTP_CLIENT_PROXY":
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
if err != nil {
return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
}
case "HTTP_CLIENT_PROXIES":
p.opts.httpClientProxies = parseStringList(value, []string{})
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "HTTP_SERVER_TIMEOUT":
@@ -259,10 +274,16 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
case "METRICS_PASSWORD_FILE":
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
case "FETCH_BILIBILI_WATCH_TIME":
p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime)
case "FETCH_NEBULA_WATCH_TIME":
p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime)
case "FETCH_ODYSEE_WATCH_TIME":
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
case "FETCH_YOUTUBE_WATCH_TIME":
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
case "YOUTUBE_API_KEY":
p.opts.youTubeApiKey = parseString(value, defaultYouTubeApiKey)
case "YOUTUBE_EMBED_URL_OVERRIDE":
p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride)
case "WATCHDOG":
+4 -20
View File
@@ -7,34 +7,18 @@ import (
"database/sql"
"fmt"
"log/slog"
"time"
// Postgresql driver import
_ "github.com/lib/pq"
)
// NewConnectionPool configures the database connection pool.
func NewConnectionPool(dsn string, minConnections, maxConnections int, connectionLifetime time.Duration) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(maxConnections)
db.SetMaxIdleConns(minConnections)
db.SetConnMaxLifetime(connectionLifetime)
return db, nil
}
// Migrate executes database migrations.
func Migrate(db *sql.DB) error {
var currentVersion int
db.QueryRow(`SELECT version FROM schema_version`).Scan(&currentVersion)
slog.Debug("Running database migrations",
driver := getDriverStr()
slog.Info("Running database migrations",
slog.Int("current_version", currentVersion),
slog.Int("latest_version", schemaVersion),
slog.String("driver", driver),
)
for version := currentVersion; version < schemaVersion; version++ {
@@ -45,7 +29,7 @@ func Migrate(db *sql.DB) error {
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
if err := migrations[version](tx); err != nil {
if err := migrations[version](tx, driver); err != nil {
tx.Rollback()
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
+320 -136
View File
@@ -5,13 +5,15 @@ package database // import "miniflux.app/v2/internal/database"
import (
"database/sql"
"miniflux.app/v2/internal/crypto"
)
var schemaVersion = len(migrations)
// Order is important. Add new migrations at the end of the list.
var migrations = []func(tx *sql.Tx) error{
func(tx *sql.Tx) (err error) {
var migrations = []func(tx *sql.Tx, driver string) error{
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TABLE schema_version (
version text not null
@@ -120,16 +122,19 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
func(tx *sql.Tx, driver string) (err error) {
if driver == "postgresql" {
sql := `
CREATE EXTENSION IF NOT EXISTS hstore;
ALTER TABLE users ADD COLUMN extra hstore;
CREATE INDEX users_extra_idx ON users using gin(extra);
`
_, err = tx.Exec(sql)
return err
`
_, err = tx.Exec(sql)
return err
}
return nil
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TABLE tokens (
id text not null,
@@ -141,7 +146,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
@@ -149,7 +154,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TABLE integrations (
user_id int not null,
@@ -170,27 +175,27 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE sessions rename to user_sessions`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
DROP TABLE tokens;
@@ -204,7 +209,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN wallabag_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN wallabag_url text default '';
@@ -216,12 +221,12 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
@@ -229,7 +234,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN nunux_keeper_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN nunux_keeper_url text default '';
@@ -238,17 +243,17 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN pocket_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN pocket_access_token text default '';
@@ -257,14 +262,14 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN username text default '';
ALTER TABLE feeds ADD COLUMN password text default '';
@@ -272,7 +277,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
@@ -281,12 +286,12 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
UPDATE
entries
@@ -296,17 +301,17 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
UPDATE users SET theme='light_serif' WHERE theme='default';
@@ -316,7 +321,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
UPDATE entries SET changed_at = published_at;
@@ -325,7 +330,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TABLE api_keys (
id serial not null,
@@ -341,7 +346,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN share_code text not null default '';
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
@@ -349,12 +354,12 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
@@ -362,52 +367,52 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE integrations DROP COLUMN fever_password`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds
ADD COLUMN blocklist_rules text not null default '',
@@ -416,12 +421,12 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
UPDATE entries SET created_at = published_at;
@@ -429,7 +434,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, driver string) (err error) {
_, err = tx.Exec(`
ALTER TABLE users
ADD column stylesheet text not null default '',
@@ -440,63 +445,69 @@ var migrations = []func(tx *sql.Tx) error{
return err
}
_, err = tx.Exec(`
DECLARE my_cursor CURSOR FOR
SELECT
id,
COALESCE(extra->'custom_css', '') as custom_css,
COALESCE(extra->'google_id', '') as google_id,
COALESCE(extra->'oidc_id', '') as oidc_id
FROM users
FOR UPDATE
`)
if err != nil {
return err
}
defer tx.Exec("CLOSE my_cursor")
for {
var (
userID int64
customStylesheet string
googleID string
oidcID string
)
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
_, err := tx.Exec(
`UPDATE
users
SET
stylesheet=$2,
google_id=$3,
openid_connect_id=$4
WHERE
id=$1
`,
userID, customStylesheet, googleID, oidcID)
if driver == "postgresql" {
_, err = tx.Exec(`
DECLARE my_cursor CURSOR FOR
SELECT
id,
COALESCE(extra->'custom_css', '') as custom_css,
COALESCE(extra->'google_id', '') as google_id,
COALESCE(extra->'oidc_id', '') as oidc_id
FROM users
FOR UPDATE
`)
if err != nil {
return err
}
defer tx.Exec("CLOSE my_cursor")
for {
var (
userID int64
customStylesheet string
googleID string
oidcID string
)
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
_, err := tx.Exec(
`UPDATE
users
SET
stylesheet=$2,
google_id=$3,
openid_connect_id=$4
WHERE
id=$1
`,
userID, customStylesheet, googleID, oidcID)
if err != nil {
return err
}
}
}
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, driver string) (err error) {
if driver == "postgresql" {
if _, err = tx.Exec(`ALTER TABLE users DROP COLUMN extra;`); err != nil {
return err
}
}
_, err = tx.Exec(`
ALTER TABLE users DROP COLUMN extra;
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
@@ -504,7 +515,7 @@ var migrations = []func(tx *sql.Tx) error{
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
CREATE TABLE acme_cache (
key varchar(400) not null primary key,
@@ -514,13 +525,13 @@ var migrations = []func(tx *sql.Tx) error{
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE feeds ADD COLUMN allow_self_signed_certificates boolean not null default false
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
@@ -528,24 +539,24 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN cookie text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE categories ADD COLUMN hide_globally boolean not null default false
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE feeds ADD COLUMN hide_globally boolean not null default false
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN telegram_bot_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN telegram_bot_token text default '';
@@ -554,7 +565,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
@@ -562,7 +573,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN googlereader_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN googlereader_username text default '';
@@ -571,7 +582,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN espial_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN espial_url text default '';
@@ -581,7 +592,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN linkding_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN linkding_url text default '';
@@ -590,38 +601,38 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE feeds ADD COLUMN url_rewrite_rules text not null default ''
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE users ADD COLUMN default_reading_speed int default 265;
ALTER TABLE users ADD COLUMN cjk_reading_speed int default 500;
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE users ADD COLUMN default_home_page text default 'unread';
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE integrations ADD COLUMN wallabag_only_url bool default 'f';
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE users ADD COLUMN categories_sorting_order text not null default 'unread_count';
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN matrix_bot_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN matrix_bot_user text default '';
@@ -632,18 +643,18 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN double_tap boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE entries ADD COLUMN tags text[] default '{}';
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE users RENAME double_tap TO gesture_nav;
ALTER TABLE users ALTER COLUMN gesture_nav SET DATA TYPE text using case when gesture_nav = true then 'tap' when gesture_nav = false then 'none' end;
@@ -652,14 +663,14 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN linkding_tags text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN no_media_player boolean default 'f';
ALTER TABLE enclosures ADD COLUMN media_progression int default 0;
@@ -667,14 +678,14 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN linkding_mark_as_unread bool default 'f';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
// Delete duplicated rows
sql := `
DELETE FROM enclosures a USING enclosures b
@@ -702,12 +713,12 @@ var migrations = []func(tx *sql.Tx) error{
return nil
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN mark_read_on_view boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN notion_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN notion_token text default '';
@@ -716,7 +727,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN readwise_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN readwise_api_key text default '';
@@ -724,7 +735,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN apprise_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN apprise_url text default '';
@@ -733,7 +744,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN shiori_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN shiori_url text default '';
@@ -743,7 +754,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN shaarli_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN shaarli_url text default '';
@@ -752,13 +763,13 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
ALTER TABLE feeds ADD COLUMN apprise_service_urls text default '';
`)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN webhook_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN webhook_url text default '';
@@ -767,7 +778,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN telegram_bot_topic_id int;
ALTER TABLE integrations ADD COLUMN telegram_bot_disable_web_page_preview bool default 'f';
@@ -776,14 +787,14 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN telegram_bot_disable_buttons bool default 'f';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
-- Speed up has_enclosure
CREATE INDEX enclosures_entry_id_idx ON enclosures(entry_id);
@@ -799,7 +810,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN rssbridge_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN rssbridge_url text default '';
@@ -807,7 +818,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
CREATE TABLE webauthn_credentials (
handle bytea primary key,
@@ -825,7 +836,7 @@ var migrations = []func(tx *sql.Tx) error{
`)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN omnivore_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN omnivore_api_key text default '';
@@ -834,7 +845,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN linkace_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN linkace_url text default '';
@@ -846,7 +857,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN linkwarden_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN linkwarden_url text default '';
@@ -855,7 +866,7 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN readeck_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN readeck_only_url bool default 'f';
@@ -866,20 +877,193 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN disable_http2 bool default 'f'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN media_playback_rate numeric default 1;`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
func(tx *sql.Tx, _ string) (err error) {
// the WHERE part speed-up the request a lot
sql := `UPDATE entries SET tags = array_remove(tags, '') WHERE '' = ANY(tags);`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
// Entry URLs can exceeds btree maximum size
// Checking entry existence is now using entries_feed_id_status_hash_idx index
_, err = tx.Exec(`DROP INDEX entries_feed_url_idx`)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN raindrop_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN raindrop_token text default '';
ALTER TABLE integrations ADD COLUMN raindrop_collection_id text default '';
ALTER TABLE integrations ADD COLUMN raindrop_tags text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN description text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE users
ADD COLUMN block_filter_entry_rules text not null default '',
ADD COLUMN keep_filter_entry_rules text not null default ''
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN betula_url text default '';
ALTER TABLE integrations ADD COLUMN betula_token text default '';
ALTER TABLE integrations ADD COLUMN betula_enabled bool default 'f';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN ntfy_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN ntfy_url text default '';
ALTER TABLE integrations ADD COLUMN ntfy_topic text default '';
ALTER TABLE integrations ADD COLUMN ntfy_api_token text default '';
ALTER TABLE integrations ADD COLUMN ntfy_username text default '';
ALTER TABLE integrations ADD COLUMN ntfy_password text default '';
ALTER TABLE integrations ADD COLUMN ntfy_icon_url text default '';
ALTER TABLE feeds ADD COLUMN ntfy_enabled bool default 'f';
ALTER TABLE feeds ADD COLUMN ntfy_priority int default '3';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN custom_js text not null default '';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN external_font_hosts text not null default '';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN cubox_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN cubox_api_link text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN discord_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN discord_webhook_link text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE integrations ADD COLUMN ntfy_internal_links bool default 'f';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN slack_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN slack_webhook_link text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN webhook_url text default '';`)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN pushover_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN pushover_user text default '';
ALTER TABLE integrations ADD COLUMN pushover_token text default '';
ALTER TABLE integrations ADD COLUMN pushover_device text default '';
ALTER TABLE integrations ADD COLUMN pushover_prefix text default '';
ALTER TABLE feeds ADD COLUMN pushover_enabled bool default 'f';
ALTER TABLE feeds ADD COLUMN pushover_priority int default '0';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN ntfy_topic text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE icons ADD COLUMN external_id text default '';
CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
DECLARE id_cursor CURSOR FOR
SELECT
id
FROM icons
WHERE external_id = ''
FOR UPDATE`)
if err != nil {
return err
}
defer tx.Exec("CLOSE id_cursor")
for {
var id int64
if err := tx.QueryRow(`FETCH NEXT FROM id_cursor`).Scan(&id); err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
_, err = tx.Exec(
`
UPDATE icons SET external_id = $1 WHERE id = $2
`,
crypto.GenerateRandomStringHex(20), id)
if err != nil {
return err
}
}
return nil
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN proxy_url text default ''`)
return err
},
}
+31
View File
@@ -0,0 +1,31 @@
//go:build !sqlite
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package database // import "miniflux.app/v2/internal/database"
import (
"database/sql"
"time"
_ "github.com/lib/pq"
)
// NewConnectionPool configures the database connection pool.
func NewConnectionPool(dsn string, minConnections, maxConnections int, connectionLifetime time.Duration) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(maxConnections)
db.SetMaxIdleConns(minConnections)
db.SetConnMaxLifetime(connectionLifetime)
return db, nil
}
func getDriverStr() string {
return "postgresql"
}
+26
View File
@@ -0,0 +1,26 @@
//go:build sqlite
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package database // import "miniflux.app/v2/internal/database"
import (
"database/sql"
"time"
_ "github.com/mattn/go-sqlite3"
)
// NewConnectionPool configures the database connection pool.
func NewConnectionPool(dsn string, _, _ int, _ time.Duration) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, err
}
return db, nil
}
func getDriverStr() string {
return "sqlite3"
}
+2 -2
View File
@@ -247,7 +247,6 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithLimit(50)
builder.WithSorting("id", model.DefaultSortingDirection)
switch {
case request.HasQueryParam(r, "since_id"):
@@ -258,6 +257,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
slog.Int64("since_id", sinceID),
)
builder.AfterEntryID(sinceID)
builder.WithSorting("id", "ASC")
}
case request.HasQueryParam(r, "max_id"):
maxID := request.QueryInt64Param(r, "max_id", 0)
@@ -324,7 +324,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
FeedID: entry.FeedID,
Title: entry.Title,
Author: entry.Author,
HTML: mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entry.Content),
HTML: mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content),
URL: entry.URL,
IsSaved: isSaved,
IsRead: isRead,
+36 -34
View File
@@ -20,11 +20,11 @@ import (
"miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/mediaproxy"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
mff "miniflux.app/v2/internal/reader/handler"
mfs "miniflux.app/v2/internal/reader/subscription"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/urllib"
"miniflux.app/v2/internal/validator"
"github.com/gorilla/mux"
@@ -684,7 +684,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
var rssBridgeURL string
if intg, err := h.store.Integration(userID); err == nil && intg != nil && intg.RSSBridgeEnabled {
@@ -736,17 +736,16 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
return store.FeedByID(userID, feedID)
}
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
func getOrCreateCategory(streamCategory Stream, store *storage.Storage, userID int64) (*model.Category, error) {
switch {
case category.ID == "":
case streamCategory.ID == "":
return store.FirstCategory(userID)
case store.CategoryTitleExists(userID, category.ID):
return store.CategoryByTitle(userID, category.ID)
case store.CategoryTitleExists(userID, streamCategory.ID):
return store.CategoryByTitle(userID, streamCategory.ID)
default:
catRequest := model.CategoryRequest{
Title: category.ID,
}
return store.CreateCategory(userID, &catRequest)
return store.CreateCategory(userID, &model.CategoryCreationRequest{
Title: streamCategory.ID,
})
}
}
@@ -828,6 +827,14 @@ func move(stream Stream, destination Stream, store *storage.Storage, userID int6
return store.UpdateFeed(feed)
}
func (h *handler) feedIconURL(f *model.Feed) string {
if f.Icon != nil && f.Icon.ExternalIconID != "" {
return config.Opts.RootURL() + route.Path(h.router, "feedIcon", "externalIconID", f.Icon.ExternalIconID)
} else {
return ""
}
}
func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
clientIP := request.ClientIP(r)
@@ -951,6 +958,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
)
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithEnclosures()
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithEntryIDs(itemIDs)
builder.WithSorting(model.DefaultSortingOrder, requestModifiers.SortDirection)
@@ -1003,28 +1011,18 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
categories = append(categories, userStarred)
}
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entry.Content)
proxyOption := config.Opts.MediaProxyMode()
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
for i := range entry.Enclosures {
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
entry.Enclosures[i].URL = mediaproxy.ProxifyAbsoluteURL(h.router, r.Host, entry.Enclosures[i].URL)
break
}
}
}
}
entry.Enclosures.ProxifyEnclosureURL(h.router)
contentItems[i] = contentItem{
ID: fmt.Sprintf(EntryIDLong, entry.ID),
Title: entry.Title,
Author: entry.Author,
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
CrawlTimeMsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()),
CrawlTimeMsec: fmt.Sprintf("%d", entry.CreatedAt.UnixMilli()),
Published: entry.Date.Unix(),
Updated: entry.Date.Unix(),
Updated: entry.ChangedAt.Unix(),
Categories: categories,
Canonical: []contentHREF{
{
@@ -1145,20 +1143,23 @@ func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
json.NotFound(w, r)
return
}
categoryRequest := model.CategoryRequest{
Title: destination.ID,
categoryModificationRequest := model.CategoryModificationRequest{
Title: model.SetOptionalField(destination.ID),
}
verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest)
if verr != nil {
json.BadRequest(w, r, verr.Error())
if validationError := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationError != nil {
json.BadRequest(w, r, validationError.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
OK(w, r)
}
@@ -1218,6 +1219,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
json.ServerError(w, r, err)
return
}
result.Subscriptions = make([]subscription, 0)
for _, feed := range feeds {
result.Subscriptions = append(result.Subscriptions, subscription{
@@ -1226,7 +1228,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
URL: feed.FeedURL,
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
HTMLURL: feed.SiteURL,
IconURL: "", // TODO: Icons are base64 encoded in the DB.
IconURL: h.feedIconURL(feed),
})
}
json.OK(w, r, result)
+3 -4
View File
@@ -6,15 +6,14 @@ package cookie // import "miniflux.app/v2/internal/http/cookie"
import (
"net/http"
"time"
"miniflux.app/v2/internal/config"
)
// Cookie names.
const (
CookieAppSessionID = "MinifluxAppSessionID"
CookieUserSessionID = "MinifluxUserSessionID"
// Cookie duration in days.
cookieDuration = 30
)
// New creates a new cookie.
@@ -25,7 +24,7 @@ func New(name, value string, isHTTPS bool, path string) *http.Cookie {
Path: basePath(path),
Secure: isHTTPS,
HttpOnly: true,
Expires: time.Now().Add(cookieDuration * 24 * time.Hour),
Expires: time.Now().Add(time.Duration(config.Opts.CleanupRemoveSessionsDays()) * 24 * time.Hour),
SameSite: http.SameSiteLaxMode,
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import "net/http"
// CookieValue returns the cookie value.
func CookieValue(r *http.Request, name string) string {
cookie, err := r.Cookie(name)
if err == http.ErrNoCookie {
if err != nil {
return ""
}
+12 -3
View File
@@ -12,6 +12,8 @@ import (
"net/http"
"strings"
"time"
"github.com/andybalholm/brotli"
)
const compressionThreshold = 1024
@@ -23,7 +25,7 @@ type Builder struct {
statusCode int
headers map[string]string
enableCompression bool
body interface{}
body any
}
// WithStatus uses the given status code to build the response.
@@ -39,7 +41,7 @@ func (b *Builder) WithHeader(key, value string) *Builder {
}
// WithBody uses the given body to build the response.
func (b *Builder) WithBody(body interface{}) *Builder {
func (b *Builder) WithBody(body any) *Builder {
b.body = body
return b
}
@@ -110,8 +112,15 @@ func (b *Builder) writeHeaders() {
func (b *Builder) compress(data []byte) {
if b.enableCompression && len(data) > compressionThreshold {
acceptEncoding := b.r.Header.Get("Accept-Encoding")
switch {
case strings.Contains(acceptEncoding, "br"):
b.headers["Content-Encoding"] = "br"
b.writeHeaders()
brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
defer brotliWriter.Close()
brotliWriter.Write(data)
return
case strings.Contains(acceptEncoding, "gzip"):
b.headers["Content-Encoding"] = "gzip"
b.writeHeaders()
+25 -1
View File
@@ -228,7 +228,7 @@ func TestBuildResponseWithCachingAndEtag(t *testing.T) {
}
}
func TestBuildResponseWithGzipCompression(t *testing.T) {
func TestBuildResponseWithBrotliCompression(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "gzip, deflate, br")
@@ -245,6 +245,30 @@ func TestBuildResponseWithGzipCompression(t *testing.T) {
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "br"
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithGzipCompression(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "gzip, deflate")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "gzip"
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
+2 -2
View File
@@ -37,7 +37,7 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error) {
builder := response.New(w, r)
builder.WithStatus(http.StatusInternalServerError)
builder.WithHeader("Content-Security-Policy", `default-src 'self'`)
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
@@ -61,7 +61,7 @@ func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
builder := response.New(w, r)
builder.WithStatus(http.StatusBadRequest)
builder.WithHeader("Content-Security-Policy", `default-src 'self'`)
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
+60 -18
View File
@@ -16,19 +16,31 @@ import (
const contentTypeHeader = `application/json`
// OK creates a new JSON response with a 200 status code.
func OK(w http.ResponseWriter, r *http.Request, body interface{}) {
func OK(w http.ResponseWriter, r *http.Request, body any) {
responseBody, err := json.Marshal(body)
if err != nil {
ServerError(w, r, err)
return
}
builder := response.New(w, r)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSON(body))
builder.WithBody(responseBody)
builder.Write()
}
// Created sends a created response to the client.
func Created(w http.ResponseWriter, r *http.Request, body interface{}) {
func Created(w http.ResponseWriter, r *http.Request, body any) {
responseBody, err := json.Marshal(body)
if err != nil {
ServerError(w, r, err)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusCreated)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSON(body))
builder.WithBody(responseBody)
builder.Write()
}
@@ -62,10 +74,17 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error) {
),
)
responseBody, jsonErr := generateJSONError(err)
if jsonErr != nil {
slog.Error("Unable to generate JSON error", slog.Any("error", jsonErr))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusInternalServerError)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSONError(err))
builder.WithBody(responseBody)
builder.Write()
}
@@ -84,10 +103,17 @@ func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
),
)
responseBody, jsonErr := generateJSONError(err)
if jsonErr != nil {
slog.Error("Unable to generate JSON error", slog.Any("error", jsonErr))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusBadRequest)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSONError(err))
builder.WithBody(responseBody)
builder.Write()
}
@@ -105,10 +131,17 @@ func Unauthorized(w http.ResponseWriter, r *http.Request) {
),
)
responseBody, jsonErr := generateJSONError(errors.New("access unauthorized"))
if jsonErr != nil {
slog.Error("Unable to generate JSON error", slog.Any("error", jsonErr))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusUnauthorized)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSONError(errors.New("access unauthorized")))
builder.WithBody(responseBody)
builder.Write()
}
@@ -126,10 +159,17 @@ func Forbidden(w http.ResponseWriter, r *http.Request) {
),
)
responseBody, jsonErr := generateJSONError(errors.New("access forbidden"))
if jsonErr != nil {
slog.Error("Unable to generate JSON error", slog.Any("error", jsonErr))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusForbidden)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSONError(errors.New("access forbidden")))
builder.WithBody(responseBody)
builder.Write()
}
@@ -147,27 +187,29 @@ func NotFound(w http.ResponseWriter, r *http.Request) {
),
)
responseBody, jsonErr := generateJSONError(errors.New("resource not found"))
if jsonErr != nil {
slog.Error("Unable to generate JSON error", slog.Any("error", jsonErr))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
builder := response.New(w, r)
builder.WithStatus(http.StatusNotFound)
builder.WithHeader("Content-Type", contentTypeHeader)
builder.WithBody(toJSONError(errors.New("resource not found")))
builder.WithBody(responseBody)
builder.Write()
}
func toJSONError(err error) []byte {
func generateJSONError(err error) ([]byte, error) {
type errorMsg struct {
ErrorMessage string `json:"error_message"`
}
return toJSON(errorMsg{ErrorMessage: err.Error()})
}
func toJSON(v interface{}) []byte {
b, err := json.Marshal(v)
encodedBody, err := json.Marshal(errorMsg{ErrorMessage: err.Error()})
if err != nil {
slog.Error("Unable to marshal JSON response", slog.Any("error", err))
return []byte("")
return nil, err
}
return b
return encodedBody, nil
}
+2 -2
View File
@@ -293,12 +293,12 @@ func TestBuildInvalidJSONResponse(t *testing.T) {
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
expectedStatusCode := http.StatusInternalServerError
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := ``
expectedBody := `{"error_message":"json: unsupported type: chan int"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
+14
View File
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package response // import "miniflux.app/v2/internal/http/response"
// ContentSecurityPolicyForUntrustedContent is the default CSP for untrusted content.
// default-src 'none' disables all content sources
// form-action 'none' disables all form submissions
// sandbox enables a sandbox for the requested resource
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
const ContentSecurityPolicyForUntrustedContent = `default-src 'none'; form-action 'none'; sandbox;`
+39 -27
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"
@@ -26,42 +27,53 @@ func NewClient(serviceURL, baseURL string) *Client {
return &Client{serviceURL, baseURL}
}
func (c *Client) SendNotification(entry *model.Entry) error {
func (c *Client) SendNotification(feed *model.Feed, entries model.Entries) error {
if c.baseURL == "" || c.servicesURL == "" {
return fmt.Errorf("apprise: missing base URL or services URL")
}
message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n"
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
if err != nil {
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
}
for _, entry := range entries {
message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n"
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
if err != nil {
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
}
requestBody, err := json.Marshal(map[string]any{
"urls": c.servicesURL,
"body": message,
})
if err != nil {
return fmt.Errorf("apprise: unable to encode request body: %v", err)
}
requestBody, err := json.Marshal(map[string]any{
"urls": c.servicesURL,
"body": message,
"title": feed.Title,
})
if err != nil {
return fmt.Errorf("apprise: unable to encode request body: %v", err)
}
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("apprise: unable to create request: %v", err)
}
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("apprise: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("apprise: unable to send request: %v", err)
}
defer response.Body.Close()
slog.Debug("Sending Apprise notification",
slog.String("apprise_url", c.baseURL),
slog.String("services_url", c.servicesURL),
slog.String("title", feed.Title),
slog.String("body", message),
slog.String("entry_url", entry.URL),
)
if response.StatusCode >= 400 {
return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("apprise: unable to send request: %v", err)
}
response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode)
}
}
return nil
+57
View File
@@ -0,0 +1,57 @@
package betula
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"miniflux.app/v2/internal/urllib"
"miniflux.app/v2/internal/version"
)
const defaultClientTimeout = 10 * time.Second
type Client struct {
url string
token string
}
func NewClient(url, token string) *Client {
return &Client{url: url, token: token}
}
func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) error {
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.url, "/save-link")
if err != nil {
return fmt.Errorf("betula: unable to generate save-link endpoint: %v", err)
}
values := url.Values{}
values.Add("url", entryURL)
values.Add("title", entryTitle)
values.Add("tags", strings.Join(tags, ","))
request, err := http.NewRequest(http.MethodPost, apiEndpoint+"?"+values.Encode(), nil)
if err != nil {
return fmt.Errorf("betula: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("betula: unable to send request: %v", err)
}
defer response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("betula: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
}
return nil
}
+70
View File
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// Cubox API documentation: https://help.cubox.cc/save/api/
package cubox // import "miniflux.app/v2/internal/integration/cubox"
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"miniflux.app/v2/internal/version"
)
const defaultClientTimeout = 10 * time.Second
type Client struct {
apiLink string
}
func NewClient(apiLink string) *Client {
return &Client{apiLink: apiLink}
}
func (c *Client) SaveLink(entryURL string) error {
if c.apiLink == "" {
return errors.New("cubox: missing API link")
}
requestBody, err := json.Marshal(&card{
Type: "url",
Content: entryURL,
})
if err != nil {
return fmt.Errorf("cubox: unable to encode request body: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultClientTimeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiLink, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("cubox: unable to create request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
response, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("cubox: unable to send request: %w", err)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return fmt.Errorf("cubox: unable to save link: status=%d", response.StatusCode)
}
return nil
}
type card struct {
Type string `json:"type"`
Content string `json:"content"`
}
+109
View File
@@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// Discord Webhooks documentation: https://discord.com/developers/docs/resources/webhook
package discord // import "miniflux.app/v2/internal/integration/discord"
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/urllib"
"miniflux.app/v2/internal/version"
)
const defaultClientTimeout = 10 * time.Second
const discordMsgColor = 5793266
type Client struct {
webhookURL string
}
func NewClient(webhookURL string) *Client {
return &Client{webhookURL: webhookURL}
}
func (c *Client) SendDiscordMsg(feed *model.Feed, entries model.Entries) error {
for _, entry := range entries {
requestBody, err := json.Marshal(&discordMessage{
Embeds: []discordEmbed{
{
Title: "RSS feed update from Miniflux",
Color: discordMsgColor,
Fields: []discordFields{
{
Name: "Updated feed",
Value: feed.Title,
},
{
Name: "Article link",
Value: "[" + entry.Title + "]" + "(" + entry.URL + ")",
},
{
Name: "Author",
Value: entry.Author,
Inline: true,
},
{
Name: "Source website",
Value: urllib.RootURL(feed.SiteURL),
Inline: true,
},
},
},
},
})
if err != nil {
return fmt.Errorf("discord: unable to encode request body: %v", err)
}
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("discord: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
slog.Debug("Sending Discord notification",
slog.String("webhookURL", c.webhookURL),
slog.String("title", feed.Title),
slog.String("entry_url", entry.URL),
)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("discord: unable to send request: %v", err)
}
response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("discord: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode)
}
}
return nil
}
type discordFields struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline,omitempty"`
}
type discordEmbed struct {
Title string `json:"title"`
Color int `json:"color"`
Fields []discordFields `json:"fields"`
}
type discordMessage struct {
Embeds []discordEmbed `json:"embeds"`
}
+182 -33
View File
@@ -8,6 +8,9 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/integration/apprise"
"miniflux.app/v2/internal/integration/betula"
"miniflux.app/v2/internal/integration/cubox"
"miniflux.app/v2/internal/integration/discord"
"miniflux.app/v2/internal/integration/espial"
"miniflux.app/v2/internal/integration/instapaper"
"miniflux.app/v2/internal/integration/linkace"
@@ -15,14 +18,18 @@ import (
"miniflux.app/v2/internal/integration/linkwarden"
"miniflux.app/v2/internal/integration/matrixbot"
"miniflux.app/v2/internal/integration/notion"
"miniflux.app/v2/internal/integration/ntfy"
"miniflux.app/v2/internal/integration/nunuxkeeper"
"miniflux.app/v2/internal/integration/omnivore"
"miniflux.app/v2/internal/integration/pinboard"
"miniflux.app/v2/internal/integration/pocket"
"miniflux.app/v2/internal/integration/pushover"
"miniflux.app/v2/internal/integration/raindrop"
"miniflux.app/v2/internal/integration/readeck"
"miniflux.app/v2/internal/integration/readwise"
"miniflux.app/v2/internal/integration/shaarli"
"miniflux.app/v2/internal/integration/shiori"
"miniflux.app/v2/internal/integration/slack"
"miniflux.app/v2/internal/integration/telegrambot"
"miniflux.app/v2/internal/integration/wallabag"
"miniflux.app/v2/internal/integration/webhook"
@@ -31,6 +38,30 @@ import (
// SendEntry sends the entry to third-party providers when the user click on "Save".
func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
if userIntegrations.BetulaEnabled {
slog.Debug("Sending entry to Betula",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
)
client := betula.NewClient(userIntegrations.BetulaURL, userIntegrations.BetulaToken)
err := client.CreateBookmark(
entry.URL,
entry.Title,
entry.Tags,
)
if err != nil {
slog.Error("Unable to send entry to Betula",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.Any("error", err),
)
}
}
if userIntegrations.PinboardEnabled {
slog.Debug("Sending entry to Pinboard",
slog.Int64("user_id", userIntegrations.UserID),
@@ -295,6 +326,25 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
}
}
if userIntegrations.CuboxEnabled {
slog.Debug("Sending entry to Cubox",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
)
client := cubox.NewClient(userIntegrations.CuboxAPILink)
if err := client.SaveLink(entry.URL); err != nil {
slog.Error("Unable to send entry to Cubox",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.Any("error", err),
)
}
}
if userIntegrations.ShioriEnabled {
slog.Debug("Sending entry to Shiori",
slog.Int64("user_id", userIntegrations.UserID),
@@ -359,6 +409,7 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
)
}
}
if userIntegrations.OmnivoreEnabled {
slog.Debug("Sending entry to Omnivore",
slog.Int64("user_id", userIntegrations.UserID),
@@ -376,6 +427,24 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
)
}
}
if userIntegrations.RaindropEnabled {
slog.Debug("Sending entry to Raindrop",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
)
client := raindrop.NewClient(userIntegrations.RaindropToken, userIntegrations.RaindropCollectionID, userIntegrations.RaindropTags)
if err := client.CreateRaindrop(entry.URL, entry.Title); err != nil {
slog.Error("Unable to send entry to Raindrop",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.Any("error", err),
)
}
}
}
// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
@@ -406,27 +475,136 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
}
if userIntegrations.WebhookEnabled {
var webhookURL string
if feed.WebhookURL != "" {
webhookURL = feed.WebhookURL
} else {
webhookURL = userIntegrations.WebhookURL
}
slog.Debug("Sending new entries to Webhook",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
slog.String("webhook_url", userIntegrations.WebhookURL),
slog.String("webhook_url", webhookURL),
)
webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
webhookClient := webhook.NewClient(webhookURL, userIntegrations.WebhookSecret)
if err := webhookClient.SendNewEntriesWebhookEvent(feed, entries); err != nil {
slog.Debug("Unable to send new entries to Webhook",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
slog.String("webhook_url", userIntegrations.WebhookURL),
slog.String("webhook_url", webhookURL),
slog.Any("error", err),
)
}
}
if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
ntfyTopic := feed.NtfyTopic
if ntfyTopic == "" {
ntfyTopic = userIntegrations.NtfyTopic
}
slog.Debug("Sending new entries to Ntfy",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
slog.String("topic", ntfyTopic),
)
client := ntfy.NewClient(
userIntegrations.NtfyURL,
ntfyTopic,
userIntegrations.NtfyAPIToken,
userIntegrations.NtfyUsername,
userIntegrations.NtfyPassword,
userIntegrations.NtfyIconURL,
userIntegrations.NtfyInternalLinks,
feed.NtfyPriority,
)
if err := client.SendMessages(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Ntfy", slog.Any("error", err))
}
}
if userIntegrations.AppriseEnabled {
slog.Debug("Sending new entries to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)
appriseServiceURLs := userIntegrations.AppriseServicesURL
if feed.AppriseServiceURLs != "" {
appriseServiceURLs = feed.AppriseServiceURLs
}
client := apprise.NewClient(
appriseServiceURLs,
userIntegrations.AppriseURL,
)
if err := client.SendNotification(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Apprise", slog.Any("error", err))
}
}
if userIntegrations.DiscordEnabled {
slog.Debug("Sending new entries to Discord",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)
client := discord.NewClient(
userIntegrations.DiscordWebhookLink,
)
if err := client.SendDiscordMsg(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Discord", slog.Any("error", err))
}
}
if userIntegrations.SlackEnabled {
slog.Debug("Sending new entries to Slack",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)
client := slack.NewClient(
userIntegrations.SlackWebhookLink,
)
if err := client.SendSlackMsg(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Slack", slog.Any("error", err))
}
}
if userIntegrations.PushoverEnabled && feed.PushoverEnabled {
slog.Debug("Sending new entries to Pushover",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)
client := pushover.New(
userIntegrations.PushoverUser,
userIntegrations.PushoverToken,
feed.PushoverPriority,
userIntegrations.PushoverDevice,
userIntegrations.PushoverPrefix,
)
if err := client.SendMessages(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Pushover", slog.Any("error", err))
}
}
// Integrations that only support sending individual entries
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
if userIntegrations.TelegramBotEnabled {
for _, entry := range entries {
if userIntegrations.TelegramBotEnabled {
slog.Debug("Sending a new entry to Telegram",
@@ -453,35 +631,6 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
)
}
}
if userIntegrations.AppriseEnabled {
slog.Debug("Sending a new entry to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.String("apprise_url", userIntegrations.AppriseURL),
)
appriseServiceURLs := userIntegrations.AppriseServicesURL
if feed.AppriseServiceURLs != "" {
appriseServiceURLs = feed.AppriseServiceURLs
}
client := apprise.NewClient(
appriseServiceURLs,
userIntegrations.AppriseURL,
)
if err := client.SendNotification(entry); err != nil {
slog.Error("Unable to send entry to Apprise",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.String("apprise_url", userIntegrations.AppriseURL),
slog.Any("error", err),
)
}
}
}
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (c *Client) AddURL(entryURL, entryTitle string) error {
return c == ',' || c == ' '
}
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v1/links")
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v2/links")
if err != nil {
return fmt.Errorf("linkace: invalid API endpoint: %v", err)
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"miniflux.app/v2/internal/model"
)
// PushEntry pushes entries to matrix chat using integration settings provided
// PushEntries pushes entries to matrix chat using integration settings provided
func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
client := NewClient(matrixBaseURL)
discovery, err := client.DiscoverEndpoints()
+132
View File
@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package ntfy // import "miniflux.app/v2/internal/integration/ntfy"
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"time"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/version"
)
const (
defaultClientTimeout = 10 * time.Second
defaultNtfyURL = "https://ntfy.sh"
)
type Client struct {
ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
ntfyInternalLinks bool
ntfyPriority int
}
func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyInternalLinks bool, ntfyPriority int) *Client {
if ntfyURL == "" {
ntfyURL = defaultNtfyURL
}
return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyInternalLinks, ntfyPriority}
}
func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
for _, entry := range entries {
ntfyMessage := &ntfyMessage{
Topic: c.ntfyTopic,
Message: entry.Title,
Title: feed.Title,
Priority: c.ntfyPriority,
Click: entry.URL,
}
if c.ntfyIconURL != "" {
ntfyMessage.Icon = c.ntfyIconURL
}
if c.ntfyInternalLinks {
url, err := url.Parse(config.Opts.BaseURL())
if err != nil {
slog.Error("Unable to parse base URL", slog.Any("error", err))
} else {
ntfyMessage.Click = fmt.Sprintf("%s%s%d", url, "/unread/entry/", entry.ID)
}
}
slog.Debug("Sending Ntfy message",
slog.String("url", c.ntfyURL),
slog.String("topic", c.ntfyTopic),
slog.Int("priority", ntfyMessage.Priority),
slog.String("message", ntfyMessage.Message),
slog.String("entry_url", ntfyMessage.Click),
)
if err := c.makeRequest(ntfyMessage); err != nil {
return err
}
}
return nil
}
func (c *Client) makeRequest(payload any) error {
requestBody, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("ntfy: unable to encode request body: %v", err)
}
request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("ntfy: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
// See https://docs.ntfy.sh/publish/#access-tokens
if c.ntfyApiToken != "" {
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.ntfyApiToken))
}
// See https://docs.ntfy.sh/publish/#username-password
if c.ntfyUsername != "" && c.ntfyPassword != "" {
request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
}
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("ntfy: unable to send request: %v", err)
}
defer response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
}
return nil
}
// See https://docs.ntfy.sh/publish/#publish-as-json
type ntfyMessage struct {
Topic string `json:"topic"`
Message string `json:"message"`
Title string `json:"title"`
Tags []string `json:"tags,omitempty"`
Priority int `json:"priority,omitempty"`
Icon string `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
Click string `json:"click,omitempty"`
Actions []ntfyAction `json:"actions,omitempty"`
}
// See https://docs.ntfy.sh/publish/#action-buttons
type ntfyAction struct {
Action string `json:"action"`
Label string `json:"label"`
URL string `json:"url"`
}
+62 -7
View File
@@ -4,6 +4,8 @@
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
import (
"encoding/xml"
"errors"
"fmt"
"net/http"
"net/url"
@@ -12,6 +14,9 @@ import (
"miniflux.app/v2/internal/version"
)
var errPostNotFound = fmt.Errorf("pinboard: post not found")
var errMissingCredentials = fmt.Errorf("pinboard: missing auth token")
const defaultClientTimeout = 10 * time.Second
type Client struct {
@@ -24,20 +29,27 @@ func NewClient(authToken string) *Client {
func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
if c.authToken == "" {
return fmt.Errorf("pinboard: missing auth token")
return errMissingCredentials
}
toRead := "no"
// We check if the url is already bookmarked to avoid overriding existing data.
post, err := c.getBookmark(entryURL)
if err != nil && errors.Is(err, errPostNotFound) {
post = NewPost(entryURL, entryTitle)
} else if err != nil {
// In case of any other error, we return immediately to avoid overriding existing data.
return err
}
post.addTag(pinboardTags)
if markAsUnread {
toRead = "yes"
post.SetToread()
}
values := url.Values{}
values.Add("auth_token", c.authToken)
values.Add("url", entryURL)
values.Add("description", entryTitle)
values.Add("tags", pinboardTags)
values.Add("toread", toRead)
post.AddValues(values)
apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
@@ -61,3 +73,46 @@ func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markA
return nil
}
// getBookmark fetches a bookmark from Pinboard. https://www.pinboard.in/api/#posts_get
func (c *Client) getBookmark(entryURL string) (*Post, error) {
if c.authToken == "" {
return nil, errMissingCredentials
}
values := url.Values{}
values.Add("auth_token", c.authToken)
values.Add("url", entryURL)
apiEndpoint := "https://api.pinboard.in/v1/posts/get?" + values.Encode()
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("pinboard: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return nil, fmt.Errorf("pinboard: unable fetch bookmark: %v", err)
}
defer response.Body.Close()
if response.StatusCode >= 400 {
return nil, fmt.Errorf("pinboard: unable to fetch bookmark, status=%d", response.StatusCode)
}
var results posts
err = xml.NewDecoder(response.Body).Decode(&results)
if err != nil {
return nil, fmt.Errorf("pinboard: unable to decode XML: %v", err)
}
if len(results.Posts) == 0 {
return nil, errPostNotFound
}
return &results.Posts[0], nil
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
import (
"encoding/xml"
"net/url"
"strings"
"time"
)
// Post a Pinboard bookmark. "inspiration" from https://github.com/drags/pinboard/blob/master/posts.go#L32-L42
type Post struct {
XMLName xml.Name `xml:"post"`
Url string `xml:"href,attr"`
Description string `xml:"description,attr"`
Tags string `xml:"tag,attr"`
Extended string `xml:"extended,attr"`
Date time.Time `xml:"time,attr"`
Shared string `xml:"shared,attr"`
Toread string `xml:"toread,attr"`
}
// Posts A result of a Pinboard API call
type posts struct {
XMLName xml.Name `xml:"posts"`
Posts []Post `xml:"post"`
}
func NewPost(url string, description string) *Post {
return &Post{
Url: url,
Description: description,
Date: time.Now(),
Toread: "no",
}
}
func (p *Post) addTag(tag string) {
if !strings.Contains(p.Tags, tag) {
p.Tags += " " + tag
}
}
func (p *Post) SetToread() {
p.Toread = "yes"
}
func (p *Post) AddValues(values url.Values) {
values.Add("url", p.Url)
values.Add("description", p.Description)
values.Add("tags", p.Tags)
if p.Toread != "" {
values.Add("toread", p.Toread)
}
if p.Shared != "" {
values.Add("shared", p.Shared)
}
values.Add("dt", p.Date.Format(time.RFC3339))
values.Add("extended", p.Extended)
}
+138
View File
@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package pushover // import "miniflux.app/v2/internal/integration/pushover"
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/version"
)
const (
defaultClientTimeout = 10 * time.Second
defaultPushoverURL = "https://api.pushover.net"
)
type Client struct {
prefix string
token string
user string
device string
priority int
}
type Message struct {
Token string `json:"token"`
User string `json:"user"`
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
URL string `json:"url"`
URLTitle string `json:"url_title"`
Device string `json:"device,omitempty"`
}
type ErrorResponse struct {
User string `json:"user"`
Errors []string `json:"errors"`
Status int `json:"status"`
Request string `json:"request"`
}
func New(user, token string, priority int, device, urlPrefix string) *Client {
if urlPrefix == "" {
urlPrefix = defaultPushoverURL
}
if priority < -2 {
priority = -2
}
if priority > 2 {
priority = 2
}
return &Client{
user: user,
token: token,
device: device,
prefix: urlPrefix,
priority: priority,
}
}
func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
if c.token == "" || c.user == "" {
return fmt.Errorf("pushover token and user are required")
}
for _, entry := range entries {
msg := &Message{
User: c.user,
Token: c.token,
Device: c.device,
Message: entry.Title,
Title: feed.Title,
Priority: c.priority,
URL: entry.URL,
}
slog.Debug("Sending Pushover message",
slog.Int("priority", msg.Priority),
slog.String("message", msg.Message),
slog.String("entry_url", msg.URL),
)
if err := c.makeRequest(msg); err != nil {
return fmt.Errorf("c.makeRequest: %w", err)
}
}
return nil
}
func (c *Client) makeRequest(payload *Message) error {
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("json.Marshal: %w", err)
}
url := c.prefix + "/1/messages.json"
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("http.NewRequest: %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Set("User-Agent", "Miniflux/"+version.Version)
httpClient := &http.Client{Timeout: defaultClientTimeout}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("httpClient.Do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
errorMessage := resp.Status
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
if len(errResp.Errors) > 0 {
errorMessage = strings.Join(errResp.Errors, ",")
}
}
return fmt.Errorf("pushover API error (%d): %s", resp.StatusCode, errorMessage)
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package raindrop // import "miniflux.app/v2/internal/integration/raindrop"
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"miniflux.app/v2/internal/version"
)
const defaultClientTimeout = 10 * time.Second
type Client struct {
token string
collectionID string
tags []string
}
func NewClient(token, collectionID, tags string) *Client {
return &Client{token: token, collectionID: collectionID, tags: strings.Split(tags, ",")}
}
// https://developer.raindrop.io/v1/raindrops/single#create-raindrop
func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
if c.token == "" {
return fmt.Errorf("raindrop: missing token")
}
var request *http.Request
requestBodyJson, err := json.Marshal(&raindrop{
Link: entryURL,
Title: entryTitle,
Collection: collection{Id: c.collectionID},
Tags: c.tags,
})
if err != nil {
return fmt.Errorf("raindrop: unable to encode request body: %v", err)
}
request, err = http.NewRequest(http.MethodPost, "https://api.raindrop.io/rest/v1/raindrop", bytes.NewReader(requestBodyJson))
if err != nil {
return fmt.Errorf("raindrop: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("Authorization", "Bearer "+c.token)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("raindrop: unable to send request: %v", err)
}
defer response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("raindrop: unable to create bookmark: status=%d", response.StatusCode)
}
return nil
}
type raindrop struct {
Link string `json:"link"`
Title string `json:"title"`
Collection collection `json:"collection,omitempty"`
Tags []string `json:"tags"`
}
type collection struct {
Id string `json:"$id"`
}
+1 -1
View File
@@ -91,7 +91,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string
contentBodyHeader, err := json.Marshal(&partContentHeader{
Url: entryURL,
ContentHeader: contentHeader{ContentType: "text/html"},
ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
})
if err != nil {
return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
+26 -12
View File
@@ -31,7 +31,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
return fmt.Errorf("shiori: missing base URL, username or password")
}
sessionID, err := c.authenticate()
token, err := c.authenticate()
if err != nil {
return fmt.Errorf("shiori: unable to authenticate: %v", err)
}
@@ -44,7 +44,11 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
requestBody, err := json.Marshal(&addBookmarkRequest{
URL: entryURL,
Title: entryTitle,
Excerpt: "",
CreateArchive: true,
CreateEbook: false,
Public: 0,
Tags: make([]string, 0),
})
if err != nil {
@@ -58,7 +62,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("X-Session-Id", sessionID)
request.Header.Set("Authorization", "Bearer "+token)
httpClient := &http.Client{Timeout: defaultClientTimeout}
@@ -75,13 +79,13 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
return nil
}
func (c *Client) authenticate() (sessionID string, err error) {
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/login")
func (c *Client) authenticate() (token string, err error) {
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v1/auth/login")
if err != nil {
return "", fmt.Errorf("shiori: invalid API endpoint: %v", err)
}
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password})
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password, RememberMe: false})
if err != nil {
return "", fmt.Errorf("shiori: unable to encode request body: %v", err)
}
@@ -111,21 +115,31 @@ func (c *Client) authenticate() (sessionID string, err error) {
if err := json.NewDecoder(response.Body).Decode(&authResponse); err != nil {
return "", fmt.Errorf("shiori: unable to decode response: %v", err)
}
return authResponse.SessionID, nil
return authResponse.Message.Token, nil
}
type authRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username"`
Password string `json:"password"`
RememberMe bool `json:"remember_me"`
}
type authResponse struct {
OK bool `json:"ok"`
Message authResponseMessage `json:"message"`
}
type authResponseMessage struct {
SessionID string `json:"session"`
Token string `json:"token"`
}
type addBookmarkRequest struct {
URL string `json:"url"`
Title string `json:"title"`
CreateArchive bool `json:"createArchive"`
URL string `json:"url"`
Title string `json:"title"`
CreateArchive bool `json:"create_archive"`
CreateEbook bool `json:"create_ebook"`
Public int `json:"public"`
Excerpt string `json:"excerpt"`
Tags []string `json:"tags"`
}
+113
View File
@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// Slack Webhooks documentation: https://api.slack.com/messaging/webhooks
package slack // import "miniflux.app/v2/internal/integration/slack"
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/urllib"
"miniflux.app/v2/internal/version"
)
const defaultClientTimeout = 10 * time.Second
const slackMsgColor = "#5865F2"
type Client struct {
webhookURL string
}
func NewClient(webhookURL string) *Client {
return &Client{webhookURL: webhookURL}
}
func (c *Client) SendSlackMsg(feed *model.Feed, entries model.Entries) error {
for _, entry := range entries {
requestBody, err := json.Marshal(&slackMessage{
Attachments: []slackAttachments{
{
Title: "RSS feed update from Miniflux",
Color: slackMsgColor,
Fields: []slackFields{
{
Title: "Updated feed",
Value: feed.Title,
},
{
Title: "Article title",
Value: entry.Title,
},
{
Title: "Article link",
Value: entry.URL,
},
{
Title: "Author",
Value: entry.Author,
Short: true,
},
{
Title: "Source website",
Value: urllib.RootURL(feed.SiteURL),
Short: true,
},
},
},
},
})
if err != nil {
return fmt.Errorf("slack: unable to encode request body: %v", err)
}
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("slack: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
slog.Debug("Sending Slack notification",
slog.String("webhookURL", c.webhookURL),
slog.String("title", feed.Title),
slog.String("entry_url", entry.URL),
)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("slack: unable to send request: %v", err)
}
response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("slack: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode)
}
}
return nil
}
type slackFields struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short,omitempty"`
}
type slackAttachments struct {
Title string `json:"title"`
Color string `json:"color"`
Fields []slackFields `json:"fields"`
}
type slackMessage struct {
Attachments []slackAttachments `json:"attachments"`
}
@@ -5,13 +5,18 @@ package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
import (
"fmt"
"log/slog"
"strconv"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/urllib"
)
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
formattedText := fmt.Sprintf(
`<a href=%q>%s</a>`,
`<b>%s</b> - <a href=%q>%s</a>`,
feed.Title,
entry.URL,
entry.Title,
)
@@ -31,8 +36,16 @@ func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, to
if !disableButtons {
var markupRow []*InlineKeyboardButton
websiteURLButton := InlineKeyboardButton{Text: "Go to website", URL: feed.SiteURL}
markupRow = append(markupRow, &websiteURLButton)
baseURL := config.Opts.BaseURL()
entryPath := "/unread/entry/" + strconv.FormatInt(entry.ID, 10)
minifluxEntryURL, err := urllib.JoinBaseURLAndPath(baseURL, entryPath)
if err != nil {
slog.Error("Unable to create Miniflux entry URL", slog.Any("error", err))
} else {
minifluxEntryURLButton := InlineKeyboardButton{Text: "Go to Miniflux", URL: minifluxEntryURL}
markupRow = append(markupRow, &minifluxEntryURLButton)
}
articleURLButton := InlineKeyboardButton{Text: "Go to article", URL: entry.URL}
markupRow = append(markupRow, &articleURLButton)
+12 -3
View File
@@ -12,17 +12,26 @@ import (
type translationDict map[string]interface{}
type catalog map[string]translationDict
var defaultCatalog catalog
var defaultCatalog = make(catalog, len(AvailableLanguages))
//go:embed translations/*.json
var translationFiles embed.FS
func GetTranslationDict(language string) (translationDict, error) {
if _, ok := defaultCatalog[language]; !ok {
var err error
if defaultCatalog[language], err = loadTranslationFile(language); err != nil {
return nil, err
}
}
return defaultCatalog[language], nil
}
// LoadCatalogMessages loads and parses all translations encoded in JSON.
func LoadCatalogMessages() error {
var err error
defaultCatalog = make(catalog, len(AvailableLanguages()))
for language := range AvailableLanguages() {
for language := range AvailableLanguages {
defaultCatalog[language], err = loadTranslationFile(language)
if err != nil {
return err
+26 -4
View File
@@ -39,7 +39,7 @@ func TestLoadCatalog(t *testing.T) {
}
func TestAllKeysHaveValue(t *testing.T) {
for language := range AvailableLanguages() {
for language := range AvailableLanguages {
messages, err := loadTranslationFile(language)
if err != nil {
t.Fatalf(`Unable to load translation messages for language %q`, language)
@@ -71,7 +71,7 @@ func TestMissingTranslations(t *testing.T) {
t.Fatal(`Unable to parse reference language`)
}
for language := range AvailableLanguages() {
for language := range AvailableLanguages {
if language == refLang {
continue
}
@@ -83,14 +83,36 @@ func TestMissingTranslations(t *testing.T) {
for key := range references {
if _, found := messages[key]; !found {
t.Fatalf(`Translation key %q not found in language %q`, key, language)
t.Errorf(`Translation key %q not found in language %q`, key, language)
}
}
}
}
func TestTranslationFilePluralForms(t *testing.T) {
for language := range AvailableLanguages() {
var numberOfPluralFormsPerLanguage = map[string]int{
"de_DE": 2,
"el_EL": 2,
"en_US": 2,
"es_ES": 2,
"fi_FI": 2,
"fr_FR": 2,
"hi_IN": 2,
"id_ID": 1,
"it_IT": 2,
"ja_JP": 1,
"nan_Latn_pehoeji": 1,
"nl_NL": 2,
"pl_PL": 3,
"pt_BR": 2,
"ro_RO": 3,
"ru_RU": 3,
"tr_TR": 2,
"uk_UA": 3,
"zh_CN": 1,
"zh_TW": 1,
}
for language := range AvailableLanguages {
messages, err := loadTranslationFile(language)
if err != nil {
t.Fatalf(`Unable to load translation messages for language %q`, language)
+22 -43
View File
@@ -3,47 +3,26 @@
package locale // import "miniflux.app/v2/internal/locale"
var numberOfPluralFormsPerLanguage = map[string]int{
"en_US": 2,
"es_ES": 2,
"fr_FR": 2,
"de_DE": 2,
"pl_PL": 3,
"pt_BR": 2,
"zh_CN": 1,
"zh_TW": 1,
"nl_NL": 2,
"ru_RU": 3,
"it_IT": 2,
"ja_JP": 1,
"tr_TR": 2,
"el_EL": 2,
"fi_FI": 2,
"hi_IN": 2,
"uk_UA": 3,
"id_ID": 1,
}
// AvailableLanguages returns the list of available languages.
func AvailableLanguages() map[string]string {
return map[string]string{
"en_US": "English",
"es_ES": "Español",
"fr_FR": "Français",
"de_DE": "Deutsch",
"pl_PL": "Polski",
"pt_BR": "Português Brasileiro",
"zh_CN": "简体中文",
"zh_TW": "繁體中文",
"nl_NL": "Nederlands",
"ru_RU": "Русский",
"it_IT": "Italiano",
"ja_JP": "日本語",
"tr_TR": "Türkçe",
"el_EL": "Ελληνικά",
"fi_FI": "Suomi",
"hi_IN": "हिन्दी",
"uk_UA": "Українська",
"id_ID": "Bahasa Indonesia",
}
// AvailableLanguages is the list of available languages.
var AvailableLanguages = map[string]string{
"de_DE": "Deutsch",
"el_EL": "Ελληνικά",
"en_US": "English",
"es_ES": "Español",
"fi_FI": "Suomi",
"fr_FR": "Français",
"hi_IN": "हिन्दी",
"id_ID": "Bahasa Indonesia",
"it_IT": "Italiano",
"ja_JP": "日本語",
"nan_Latn_pehoeji": "Pe̍h-ōe-jī",
"nl_NL": "Nederlands",
"pl_PL": "Polski",
"pt_BR": "Português Brasileiro",
"ro_RO": "Română",
"ru_RU": "Русский",
"tr_TR": "Türkçe",
"uk_UA": "Українська",
"zh_CN": "简体中文",
"zh_TW": "繁體中文",
}
+1 -1
View File
@@ -6,7 +6,7 @@ package locale // import "miniflux.app/v2/internal/locale"
import "testing"
func TestAvailableLanguages(t *testing.T) {
results := AvailableLanguages()
results := AvailableLanguages
for k, v := range results {
if k == "" {
t.Errorf(`Empty language key detected`)
+14 -1
View File
@@ -5,7 +5,7 @@ package locale // import "miniflux.app/v2/internal/locale"
// See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html
// And http://www.unicode.org/cldr/charts/29/supplemental/language_plural_rules.html
var pluralForms = map[string](func(n int) int){
var pluralForms = map[string]func(n int) int{
// nplurals=2; plural=(n != 1);
"default": func(n int) int {
if n != 1 {
@@ -71,6 +71,16 @@ var pluralForms = map[string](func(n int) int){
}
return 0
},
// nplurals=3; plural=(n==1 ? 0 : n==0 || (n%100 > 0 && n%100 < 20) ? 1 : 2);
"ro_RO": func(n int) int {
switch {
case n == 1:
return 0
case n == 0 || (n%100 > 0 && n%100 < 20):
return 1
}
return 2
},
"ru_RU": pluralFormRuSrUa,
// nplurals=2; plural=(n > 1);
"tr_TR": func(n int) int {
@@ -88,6 +98,9 @@ var pluralForms = map[string](func(n int) int){
"zh_TW": func(n int) int {
return 0
},
"nan_Latn_pehoeji": func(n int) int {
return 0
},
}
// nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
+9
View File
@@ -49,6 +49,11 @@ func TestPluralRules(t *testing.T) {
2: 1,
5: 1,
},
"ro_RO": {
1: 0,
2: 1,
5: 1,
},
"ru_RU": {
1: 0,
2: 1,
@@ -77,6 +82,10 @@ func TestPluralRules(t *testing.T) {
1: 0,
5: 0,
},
"nan_Latn_pehoeji": {
1: 0,
5: 0,
},
}
for rule, values := range scenarios {
+19 -14
View File
@@ -11,9 +11,11 @@ type Printer struct {
}
func (p *Printer) Print(key string) string {
if str, ok := defaultCatalog[p.language][key]; ok {
if translation, ok := str.(string); ok {
return translation
if dict, err := GetTranslationDict(p.language); err == nil {
if str, ok := dict[key]; ok {
if translation, ok := str.(string); ok {
return translation
}
}
}
return key
@@ -21,16 +23,16 @@ func (p *Printer) Print(key string) string {
// Printf is like fmt.Printf, but using language-specific formatting.
func (p *Printer) Printf(key string, args ...interface{}) string {
var translation string
translation := key
str, found := defaultCatalog[p.language][key]
if !found {
translation = key
} else {
var valid bool
translation, valid = str.(string)
if !valid {
translation = key
if dict, err := GetTranslationDict(p.language); err == nil {
str, found := dict[key]
if found {
var valid bool
translation, valid = str.(string)
if !valid {
translation = key
}
}
}
@@ -39,9 +41,12 @@ func (p *Printer) Printf(key string, args ...interface{}) string {
// Plural returns the translation of the given key by using the language plural form.
func (p *Printer) Plural(key string, n int, args ...interface{}) string {
choices, found := defaultCatalog[p.language][key]
dict, err := GetTranslationDict(p.language)
if err != nil {
return key
}
if found {
if choices, found := dict[key]; found {
var plurals []string
switch v := choices.(type) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,592 @@
{
"action.cancel": "Chhú-siau",
"action.download": "Lia̍h----loh-lâi",
"action.edit": "Pian-chi̍p",
"action.home_screen": "Chng tī chú ōe-bīn",
"action.import": "Hōe--li̍p",
"action.login": "Teng-lo̍k",
"action.or": "ah-sī",
"action.remove": "Thâi tiāu",
"action.remove_feed": "Thâi tiāu chit ê siau-sit lâi-goân",
"action.save": "Pó-chûn",
"action.subscribe": "Tēng",
"action.update": "Ōaⁿ-sin",
"alert.account_linked": "Í-keng kah lí ê gōa-pō͘ kháu-chō kiat chòe-hé--ah!",
"alert.account_unlinked": "Kah lí ê gōa-pō͘ kháu-chō ê kiat í-keng phah khui--ah!",
"alert.background_feed_refresh": "Tng leh pōe-āu ōaⁿ-sin só͘-ū siau-sit lâi-goân, lí ē-sái kè-sio̍k sú-iōng Miniflux。",
"alert.feed_error": "Chit ê siau-sit lâi-goân ū būn-tôe",
"alert.no_bookmark": "Chit-má ah bô siu-chông",
"alert.no_category": "Chit-má ah bô lūi-pia̍t",
"alert.no_category_entry": "Chit ê lūi-pah ah bô siau-sit",
"alert.no_feed": "Chit-má ah bô siau-sit lâi-goân",
"alert.no_feed_entry": "Chit ê siau-sit lâi-goân lāi bô siau-sit",
"alert.no_feed_in_category": "Bô chit ê lūi-pia̍t ê siau-sit lâi-goân",
"alert.no_history": "Chit-má ah bô kì-lo̍k",
"alert.no_search_result": "Bô hû-ha̍p ê chhiau-chhē kiat-kó",
"alert.no_shared_entry": "Chit-má ah bô hun-hióng ê siau-sit",
"alert.no_tag_entry": "Bô kah chit ê khan-á ū hû-ha̍p ê siau-sit",
"alert.no_unread_entry": "Chit-má ah-bô tha̍k kè ê siau-sit",
"alert.no_user": "Lí sī ûi-it ê sú-iōng-lâng",
"alert.pocket_linked": "Í-keng kā lí ê Pocket kháu-chō kiat chòe-hé--ah!",
"alert.prefs_saved": "Siat-tēng í-keng pó-chûn--ah!",
"alert.too_many_feeds_refresh": [
"Lí í-keng ín-khí siuⁿ chōe pái siau-sit lâi-goân ōaⁿ-sin, chhiáⁿ tán-hāu %d hun-cheng āu koh chhì-khòaⁿ-māi."
],
"confirm.loading": "Tng leh chip-hêng…",
"confirm.no": "Hóⁿ",
"confirm.question": "Kám ū khak-tēng?",
"confirm.question.refresh": "Kám beh kiông-chè têng lia̍h?",
"confirm.yes": "Sī",
"enclosure_media_controls.seek": "sóa-ūi:",
"enclosure_media_controls.seek.title": "sóa %s bió",
"enclosure_media_controls.speed": "Sok-tō͘",
"enclosure_media_controls.speed.faster": "Cheng-ka sok-tō͘",
"enclosure_media_controls.speed.faster.title": "Cheng-ka sok-tō͘ %sx",
"enclosure_media_controls.speed.reset": "Têng siat-tēng",
"enclosure_media_controls.speed.reset.title": "Têng siat-tēng pàng ê sok-tō͘ chòe 1x",
"enclosure_media_controls.speed.slower": "Pàng bān",
"enclosure_media_controls.speed.slower.title": "Pàng bān %sx",
"entry.bookmark.toast.off": "Chhú-siau siu-chông chòe soah",
"entry.bookmark.toast.on": "Sin cheng-ka siu-chông chòe soah",
"entry.bookmark.toggle.off": "Chhú-siau siu-chông",
"entry.bookmark.toggle.on": "Siu-chông khí-lâi",
"entry.comments.label": "Hôe-èng",
"entry.comments.title": "Khòaⁿ hôe-èng",
"entry.estimated_reading_time": [
"Ài %d hun-cheng lâi tha̍k"
],
"entry.external_link.label": "Gōa-pō͘ liân-kiat",
"entry.save.completed": "Pó-chûn chò soah",
"entry.save.label": "Pó-chûn",
"entry.save.title": "Pó-chûn chit ê siau-sit",
"entry.save.toast.completed": "Pó-chûn chò soah",
"entry.scraper.completed": "Lia̍h soah--ah",
"entry.scraper.label": "Lia̍h--lo̍h-lâi",
"entry.scraper.title": "Lia̍h goân-tóe lōe-iông",
"entry.share.label": "Hun-hióng",
"entry.share.title": "Hun-hióng chit ê siau-sit",
"entry.shared_entry.label": "Hun-hióng",
"entry.shared_entry.title": "Phah khui kong-khai ê liân-kiat",
"entry.state.loading": "Tng leh chip-hêng…",
"entry.state.saving": "Tng leh pó-chûn…",
"entry.status.mark_as_read": "Chù chòe tha̍k kè",
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
"entry.status.title": "Kái chōng-thài",
"entry.status.toast.read": "Chù chòe tha̍k kè chòe soah",
"entry.status.toast.unread": "Chù chòe ah-bōe tha̍k chòe soah",
"entry.tags.label": "Khan-á:",
"entry.unshare.label": "Chhú-siau hun-hióng",
"error.api_key_already_exists": "Chit ê API só-sî í-keng chûn-chāi",
"error.bad_credentials": "M̄-tio̍h ê kháu-chō miâ ah-sī bi̍t-bé.",
"error.category_already_exists": "Lūi-pia̍t í-keng chûn-chāi.",
"error.category_not_found": "Chit ê lūi-pia̍t bô chûn-chāi ah-sī bô sio̍k-tī lí.",
"error.database_error": "Chu-liāu khò͘ ū m̄-tiō: %v.",
"error.different_passwords": "Su-li̍p ê bi̍t-bé chit nn̄g pái bô kâng.",
"error.duplicate_fever_username": "Fever ê kháu-chō miâ í-keng hō͘ lâng iōng khì--ah!",
"error.duplicate_googlereader_username": "Google Reader ê kháu-chō miâ í-keng hō͘ lâng iōng khì--ah!",
"error.duplicate_linked_account": "Chit ê beh kiat chòe-hé--ê í-keng seng hō͘ lâng kiat khì--ah!",
"error.duplicated_feed": "Chit ê siau-sit lâi-goân í-keng chûn-chāi.",
"error.empty_file": "Chit ê tóng-àn sī khang--ê.",
"error.entries_per_page_invalid": "Ta̍k ia̍h ê siau-sit sò͘ ū būn-tôe.",
"error.feed_already_exists": "Chit ê siau-sit lâi-goân í-keng chûn-chāi.",
"error.feed_category_not_found": "Bô chit ê lūi-pia̍t ah-sī kóng bô sio̍k-tī chit ê sú-iōng-lâng.",
"error.feed_format_not_detected": "Bōe līn chit ê siau-sit lâi-goân ê keh-sek: %v.",
"error.feed_invalid_blocklist_rule": "Hong-só kui-chek bô-hāu.",
"error.feed_invalid_keeplist_rule": "Pó-liû kui-chek bô-hāu.",
"error.feed_mandatory_fields": "Tio̍h-ài su-lip bāng-chí kah lūi-pia̍t.",
"error.feed_not_found": "Chhē bô chit ê siau-sit lâi-goân ah-sī bô sio̍k-tī lí",
"error.feed_title_not_empty": "Beh tēng ê siau-sit lâi-goân ê piau-tôe bōe-sái sī khang--ê.",
"error.feed_url_not_empty": "Beh tēng ê siau-sit lâi-goân bāng-chí bōe-sái sī khang--ê.",
"error.fields_mandatory": "Tio̍h-ài kā chu-liāu lóng siá chê.",
"error.http_bad_gateway": "Chit ê bāng-chām chit-má in-ūi gateway ū būn-tôe bô-hoat-tō͘ iōng, m̄ sī Miniflux chia ê būn-tôe, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
"error.http_body_read": "Bô-hoat-tō͘ tha̍k HTTP body lōe-iông: %v。",
"error.http_client_error": "HTTP kheh-hō͘ thâu ū m̄-tio̍h: %v.",
"error.http_empty_response": "HTTP hôe-èng lōe-iông sī khang--ê, ū khó-lêng sī hit ê bāng-chām ū pó-hō͘ ki-chè.",
"error.http_empty_response_body": "HTTP hôe-èng body sī khang--ê.",
"error.http_forbidden": "Hō͘ kū-choa̍t chûn-chhú chit ê bāng-chām, ū khó-lêng chit ê bāng-chām ū pó-hō͘ ki-chè.",
"error.http_gateway_timeout": "Tán chit ê bāng-chām ê hôe-èng í-keng chhiau-kè sî-kan, m̄ sī Miniflux chia ê būn-tôe, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
"error.http_internal_server_error": "Chit ê bāng-chām ê su-hāu-khì in ka-kī ū būn-tôe, m̄ sī Miniflux chia ê būn-tôe, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
"error.http_not_authorized": "Bô khoân chûn-chhú chit ê bāng-chām, chhiáⁿ kiám-cha kháu-chō miâ kah bi̍t-bé。",
"error.http_resource_not_found": "Chhē bô chit ê liân-kiat, chhiáⁿ khak-līn bāng-chí kám ū chèng-khak.",
"error.http_response_too_large": "HTTP hôe-èng siuⁿ tōa. Lí ē-sái tī choân-he̍k siat-tēng lāi kā siōng koân hān-tō͘ kái khah koân (ài têng khui su-hāu-khì)。",
"error.http_service_unavailable": "Chit ê bāng-chām in-ūi in ka-kī lāi-pō͘ ū būn-tôem̄ sī Miniflux chia ê būn-tôe, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
"error.http_too_many_requests": "Miniflux tùi chit ê bāng-chām ê chhéng-kiû siuⁿ kè chōe, chhiáⁿ têng chhì-khòaⁿ-māi ah-sī tiâu-chéng thêng-sek siat-tēng.",
"error.http_unexpected_status_code": "Chit ê bāng-chām chòe liáu chi̍t ê liāu-bōe-tio̍h ê HTTP chōng-thài bé: %d, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
"error.invalid_default_home_page": "Ū-siat chú-ia̍h ū būn-tôe!",
"error.invalid_display_mode": "Ū būn-tôe ê su-li̍p bô͘-sek.",
"error.invalid_entry_direction": "Ū būn-tôe ê su-li̍p hong-hiòng.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Beh tēng ê siau-sit lâi-goân ê bāng-chí ū būn-tôe.",
"error.invalid_gesture_nav": "Chhiú-sè tō-lám ū būn-tôe.",
"error.invalid_language": "Ū būn-tôe ê gú-giân.",
"error.invalid_site_url": "Siau-sit lâi-goân ê bāng-chām ê bāng-chí ū būn-tôe.",
"error.invalid_theme": "Ū būn-tôe ê chú-tôe.",
"error.invalid_timezone": "Ū būn-tôe ê sî-khu.",
"error.network_operation": "Miniflux bô-hoat-tō͘ liân kàu chit ê bāng-chām, ū khó-lêng sī bāng-lō͘ būn-tôe: %v.",
"error.network_timeout": "Chit ê bāng-chām ê hôe-èng siuⁿ bān, chhéng-kiû chhiau-kè sî-kan: %v.",
"error.password_min_length": "Chhiáⁿ chì-chió ài su-li̍p la̍k ê lī goân.",
"error.pocket_access_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h access token",
"error.pocket_request_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h request token",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Bô-hāu ê hong-só kui-chek: kui-chek #%d khiàm ū-hāu ê lân-ūi miâ (e-sai ê soán-hāng: %s)",
"error.settings_block_rule_invalid_regex": "Bô-hāu ê hong-só kui-chek: kui-chek #%d ê bô͘-sek m̄ sī ha̍p-hoat ê chiàⁿ-kui piáu-ta̍t sek",
"error.settings_block_rule_regex_required": "Bô-hāu ê hong-só kui-chek: kui-chek #%d bô thê-kiong chiàⁿ-kui piáu-ta̍t sek",
"error.settings_block_rule_separator_required": "Bô-hāu ê hong-só kui-chek: kui-chek #%d ê bô͘-sek tio̍h-ài iōng '=' keh khui.",
"error.settings_invalid_domain_list": "Bāng-he̍k chheng-toaⁿ ū būn-tôe, chhiáⁿ iōng khang-keh keh khui bô kâng ê bāng-he̍k.",
"error.settings_keep_rule_fieldname_invalid": "Bô-hāu ê pó-liû kui-chek: kui-chek #%d khiàm ū-hāu ê lân-ūi miâ (e-sai ê soán-hāng: %s)",
"error.settings_keep_rule_invalid_regex": "Bô-hāu ê pó-liû kui-chek: kui-chek #%d d ê bô͘-sek m̄ sī ha̍p-hoat ê chiàⁿ-kui piáu-ta̍t sek",
"error.settings_keep_rule_regex_required": "Bô-hāu ê pó-liû kui-chek: kui-chek #%d bô thê-kiong chiàⁿ-kui piáu-ta̍t sek",
"error.settings_keep_rule_separator_required": "Bô-hāu ê pó-liû kui-chek: kui-chek #%d ê bô͘-sek tio̍h-ài iōng '=' keh khui.",
"error.settings_mandatory_fields": "Tio̍h-ài su-li̍p kháu-chō miâ, chú-tôe, gú-giân, sî-khu.",
"error.settings_media_playback_rate_range": "Pàng ê sok-tō͘ chhiau-kè hoān-ûi",
"error.settings_reading_speed_is_positive": "Tha̍k ê sok-tō͘ tio̍h-ài sī chiaⁿ chéng-sò͘",
"error.site_url_not_empty": "Siau-sit lâi-goân ê bāng-chām ê bāng-chí bōe-sái sī khang--ê.",
"error.subscription_not_found": "Chhē bōe tio̍h līm-hô tēng ê siau-sit lâi-goân",
"error.title_required": "Tio̍h-ài su-li̍p piau-tôe.",
"error.tls_error": "TLS m̄-tio̍h: %q。Nā-sī beh pàng-ba̍k TSL chèng-bêng, ē-sái tī siau-sit lâi-goân siat-tēng lāi thêng-tiong.",
"error.unable_to_create_api_key": "Bô-hoat-tō͘ sin cheng-ka chit ê API só-sî.",
"error.unable_to_create_category": "Bô-hoat-tō͘ sin cheng-ka chit ê lūi-pia̍t",
"error.unable_to_create_user": "Bô-hoat-tō͘ sin cheng-ka chit ê sú-iōng-lâng",
"error.unable_to_detect_rssbridge": "Sú-iōng RSS-Bridge sî chhē bô līm-hô siau-sit lâi-goân: %v.",
"error.unable_to_parse_feed": "Bô-hoat-tō͘ kái-sek chit ê siau-sit lâi-goân: %v.",
"error.unable_to_update_category": "Bô-hoat-tō͘ ōaⁿ-sin chit ê lūi-pia̍t",
"error.unable_to_update_feed": "Bô-hoat-tō͘ ōaⁿ-sin chit ê siau-sit lâi-goân",
"error.unable_to_update_user": "Bô-hoat-tō͘ ōaⁿ-sin chit ê sú-iōng-lâng",
"error.unlink_account_without_password": "Lí it-tēng ài siat-tēng bi̍t-bé, bô lí ē bô-hoat-tō͘ koh teng-lo̍k.",
"error.user_already_exists": "Chit ê sú-iōng-lâng í-keng chûn-chāi.",
"error.user_mandatory_fields": "Tio̍h-ài su-li̍p kháu-chō miâ",
"form.api_key.label.description": "API só-sîkhan-á",
"form.category.hide_globally": "Mài hián-sī siau-sit tī choân-he̍k ah-bōe tha̍k lia̍t-pió lāi",
"form.category.label.title": "Piau-tôe",
"form.feed.fieldset.general": "Thong-iōng",
"form.feed.fieldset.integration": "Tē-saⁿ hong ho̍k-bū",
"form.feed.fieldset.network_settings": "Bāng-lō͘ siat-tēng",
"form.feed.fieldset.rules": "Kui-chek",
"form.feed.label.allow_self_signed_certificates": "ún-chún chū chhiam ah-sī bô-hāu ê pîn-chèng",
"form.feed.label.apprise_service_urls": "Sú-iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
"form.feed.label.blocklist_rules": "Hong-só kui-chek",
"form.feed.label.category": "lūi-pia̍t",
"form.feed.label.cookie": "Siat-tēng Cookies",
"form.feed.label.crawler": "Lia̍h goân-tóe lōe-iông",
"form.feed.label.description": "Biâu-su̍t",
"form.feed.label.disable_http2": "Thêng iōng HTTP/2 pī-bián chéng-thâu-á-hûn tui-chong",
"form.feed.label.disabled": "Mài tha̍k chit ê siau-sit lâi-goân ê sin siau-sit",
"form.feed.label.feed_password": "Siau-sit lâi-goân bi̍t-bé",
"form.feed.label.feed_url": "Siau-sit lâi-goân bāng-chí",
"form.feed.label.feed_username": "Siau-sit lâi-goân kháu-chō miâ",
"form.feed.label.fetch_via_proxy": "Iōng tī su-hāu-khì siat-tēng ê proxy",
"form.feed.label.hide_globally": "Tī choân-he̍k ah-bōe tha̍k--ê lia̍t-pió am-khàm siau-sit",
"form.feed.label.ignore_http_cache": "Pàng-ba̍k HTTP cache",
"form.feed.label.keeplist_rules": "Pó-liû kui-chek",
"form.feed.label.no_media_player": "Bô mûi-thé hòng-sàng khì (im-sìn, sī-sìn)",
"form.feed.label.ntfy_activate": "Thui-sàng siau-sit khì ntfy",
"form.feed.label.ntfy_default_priority": "Ntfy ū-siat iu-sian sūn-sū",
"form.feed.label.ntfy_high_priority": "Ntfy koân iu-sian sūn-sū",
"form.feed.label.ntfy_low_priority": "Ntfy kē iu-sian sūn-sū",
"form.feed.label.ntfy_max_priority": "Ntfy siōng koân iu-sian sūn-sū",
"form.feed.label.ntfy_min_priority": "Ntfy siōng kē iu-sian sūn-sū",
"form.feed.label.ntfy_priority": "Ntfy iu-sian sūn-sū",
"form.feed.label.ntfy_topic": "Ntfy topic (soán thiⁿ)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to Pushover",
"form.feed.label.pushover_default_priority": "Default priority",
"form.feed.label.pushover_high_priority": "High priority",
"form.feed.label.pushover_low_priority": "Low priority",
"form.feed.label.pushover_max_priority": "Max priority",
"form.feed.label.pushover_min_priority": "Minimal priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.rewrite_rules": "Têng siá kui-chek",
"form.feed.label.scraper_rules": "Lia̍h ê kui-chek",
"form.feed.label.site_url": "Bāng-chām bāng-chí",
"form.feed.label.title": "Piau-tôe",
"form.feed.label.urlrewrite_rules": "Bāng-chí têng siá kui-chek",
"form.feed.label.user_agent": "Ngī kái sú-iōng-lâng tāi-lí",
"form.feed.label.webhook_url": "Ngī kái webhook bāng-chí",
"form.import.label.file": "OPML tóng-àn",
"form.import.label.url": "URL",
"form.integration.apprise_activate": "Thui sàng siau-sit khì Apprise",
"form.integration.apprise_services_url": "Iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
"form.integration.apprise_url": "Apprise API bāng-chí",
"form.integration.betula_activate": "Pó-chûn siau-sit kàu Betula",
"form.integration.betula_token": "Betula Token",
"form.integration.betula_url": "Betula su-hāu-khì bāng-chí",
"form.integration.cubox_activate": "Pó-chûn siau-sit khì Cubox",
"form.integration.cubox_api_link": "Cubox API liân-kiat",
"form.integration.discord_activate": "Thui-sàng siau-sit kàu Discord",
"form.integration.discord_webhook_link": "Discord Webhook liân-kiat",
"form.integration.espial_activate": "Pó-chûn siau-sit kàu Espial",
"form.integration.espial_api_key": "Espial API só-sî",
"form.integration.espial_endpoint": "Espial API thâu",
"form.integration.espial_tags": "Espial khan-á",
"form.integration.fever_activate": "Khai-sí iōng Fever API",
"form.integration.fever_endpoint": "Fever API thâu",
"form.integration.fever_password": "Fever bi̍t-bé",
"form.integration.fever_username": "Fever kháu-chō miâ",
"form.integration.googlereader_activate": "Khai-sí iōng Google Reader API",
"form.integration.googlereader_endpoint": "Google Reader API thâu",
"form.integration.googlereader_password": "Google Reader bi̍t-bé",
"form.integration.googlereader_username": "Google Reader Kháu-chō miâ",
"form.integration.instapaper_activate": "Pó-chûn siau-sit kàu Instapaper",
"form.integration.instapaper_password": "Instapaper bi̍t-bé",
"form.integration.instapaper_username": "Instapaper Kháu-chō miâ",
"form.integration.linkace_activate": "Pó-chûn siau-sit kàu LinkAce",
"form.integration.linkace_api_key": "LinkAce API só-sî",
"form.integration.linkace_check_disabled": "Thêng iōng liân-kiat kiám-cha",
"form.integration.linkace_endpoint": "LinkAce API thâu",
"form.integration.linkace_is_private": "Chù chòe su-lîn ê liân-kiat",
"form.integration.linkace_tags": "LinkAce khan-á",
"form.integration.linkding_activate": "Pó-chûn siau-sit kàu Linkding",
"form.integration.linkding_api_key": "Linkding API só-sî",
"form.integration.linkding_bookmark": "Chù chòe ah-bōe tha̍k",
"form.integration.linkding_endpoint": "Linkding API thâu",
"form.integration.linkding_tags": "Linkding khan-á",
"form.integration.linkwarden_activate": "Pó-chûn siau-sit kàu Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API só-sî",
"form.integration.linkwarden_endpoint": "Linkwarden API thâu",
"form.integration.matrix_bot_activate": "Thui-sàng siau-sit kàu Matrix",
"form.integration.matrix_bot_chat_id": "Matrix pâng-keng ID",
"form.integration.matrix_bot_password": "Matrix bi̍t-bé",
"form.integration.matrix_bot_url": "Matrix su-hāu-khìbāng-chí",
"form.integration.matrix_bot_user": "Matrix kháu-chō miâ",
"form.integration.notion_activate": "Pó-chûn siau-sit kàu Notion",
"form.integration.notion_page_id": "Notion Page ID",
"form.integration.notion_token": "Notion Secret Token",
"form.integration.ntfy_activate": "Thui-sàng siau-sit kàu Ntfy",
"form.integration.ntfy_api_token": "Ntfy API só-sî (soán thiⁿ)",
"form.integration.ntfy_icon_url": "Ntfy Icon bāng-chí (soán thiⁿ)",
"form.integration.ntfy_internal_links": "Tiám ê sî-chūn iōng lāi-pō͘ liân-kiat (soán thiⁿ)",
"form.integration.ntfy_password": "Ntfy bi̍t-bé (soán thiⁿ)",
"form.integration.ntfy_topic": "Ntfy topic (chhī-liāu nā bô siat-tēng, tiō iōng ī-siat-ti̍t)",
"form.integration.ntfy_url": "Ntfy bāng-chí (soán thiⁿ, ū-siat sī ntfy.sh)",
"form.integration.ntfy_username": "Ntfy kháu-chō miâ (soán thiⁿ)",
"form.integration.nunux_keeper_activate": "Pó-chûn siau-sit kàu Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Nunux Keeper API só-sî",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API thâu",
"form.integration.omnivore_activate": "Pó-chûn siau-sit kàu Omnivore",
"form.integration.omnivore_api_key": "Omnivore API só-sî",
"form.integration.omnivore_url": "Omnivore API thâu",
"form.integration.pinboard_activate": "Pó-chûn siau-sit kàu Pinboard",
"form.integration.pinboard_bookmark": "Chù chòe ah-bōe tha̍k",
"form.integration.pinboard_tags": "Pinboard khan-á",
"form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pocket_access_token": "Pocket token",
"form.integration.pocket_activate": "Pó-chûn siau-sit kàu Pocket",
"form.integration.pocket_connect_link": "Kah Pocket kháu-chō kiat chòe-hé",
"form.integration.pocket_consumer_key": "Pocket sú-iōng-lâng só-sî",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.raindrop_activate": "Pó-chûn siau-sit kàu Raindrop",
"form.integration.raindrop_collection_id": "Collection ID",
"form.integration.raindrop_tags": "khan-á (iōng tō͘-tiám keh khui)",
"form.integration.raindrop_token": "Raindrop Acess Token",
"form.integration.readeck_activate": "Pó-chûn siau-sit kàu Readeck",
"form.integration.readeck_api_key": "Readeck API só-sî",
"form.integration.readeck_endpoint": "Readeck API thâu",
"form.integration.readeck_labels": "Readeck Labels",
"form.integration.readeck_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
"form.integration.readwise_activate": "Pó-chûn siau-sit kàu Readwise Reader",
"form.integration.readwise_api_key": "Readwise Reader Acess Token",
"form.integration.readwise_api_key_link": "Chhú-tek lí ê Readwise Acess Token",
"form.integration.rssbridge_activate": "Sin cheng-ka siau-sit lâi-goân ê sî tio̍h RSS-Bridge",
"form.integration.rssbridge_url": "RSS-Bridge su-hāu-khì的bāng-chí",
"form.integration.shaarli_activate": "Pó-chûn siau-sit kàu Shaarli",
"form.integration.shaarli_api_secret": "Shaarli API só-sî",
"form.integration.shaarli_endpoint": "Shaarli bāng-chí",
"form.integration.shiori_activate": "Pó-chûn siau-sit kàu Shiori",
"form.integration.shiori_endpoint": "Shiori API thâu",
"form.integration.shiori_password": "Shiori bi̍t-bé",
"form.integration.shiori_username": "Shiori kháu-chō miâ",
"form.integration.slack_activate": "Thui-sàng siau-sit kàu Slack",
"form.integration.slack_webhook_link": "Slack Webhook liân-kiat",
"form.integration.telegram_bot_activate": "Thui-sàng siau-sit kàu Telegram",
"form.integration.telegram_bot_disable_buttons": "Mài hián-sī khai-koan",
"form.integration.telegram_bot_disable_notification": "Têng iōng thong-ti",
"form.integration.telegram_bot_disable_web_page_preview": "Thêng iōng bāng-ia̍h ū-lám",
"form.integration.telegram_bot_token": "Bot Token",
"form.integration.telegram_chat_id": "Chat ID",
"form.integration.telegram_topic_id": "Topic ID",
"form.integration.wallabag_activate": "Pó-chûn siau-sit kàu Wallabag",
"form.integration.wallabag_client_id": "Wallabag kheh-hō͘ thâu ID",
"form.integration.wallabag_client_secret": "Wallabag kheh-hō͘ thâu só-sî",
"form.integration.wallabag_endpoint": "Wallabag Base URL",
"form.integration.wallabag_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
"form.integration.wallabag_password": "Wallabag bi̍t-bé",
"form.integration.wallabag_username": "Wallabag kháu-chō miâ",
"form.integration.webhook_activate": "Khai-sí iōng Webhooks",
"form.integration.webhook_secret": "Webhooks Secret",
"form.integration.webhook_url": "Default Webhook bāng-chí",
"form.prefs.fieldset.application_settings": "Èng-iōng thêng-sek siat-tēng",
"form.prefs.fieldset.authentication_settings": "Sú-iōng-lâng giām-chèng siat-tēng",
"form.prefs.fieldset.global_feed_settings": "Choân-he̍k siau-sit lâi-goân siat-tēng",
"form.prefs.fieldset.reader_settings": "Ia̍t-tha̍k khì siat-tēng",
"form.prefs.help.external_font_hosts": "Iōng khang-keh keh khui ún-chún ê gōa-pō͘ lī-hêng lâi-goân. Phì-lû \"fonts.gstatic.com fonts.googleapis.com\"",
"form.prefs.label.categories_sorting_order": "Lūi-pia̍t hián-sī sūn-sū",
"form.prefs.label.cjk_reading_speed": "Tiong-bûn, Hân-bûn, Li̍t-bûn tha̍k ê sok-tō͘ (múi hun-cheng ē-sái tha̍k kúi ê lī-goân)",
"form.prefs.label.custom_css": "Chū tēng ê CSS",
"form.prefs.label.custom_js": "Chū tēng ê JavaScript",
"form.prefs.label.default_home_page": "Ū-siat chú-ia̍h",
"form.prefs.label.default_reading_speed": "Kî-thaⁿ gú-giân tha̍k ê sok-tō͘ (múi hun-cheng ē-sái tha̍k kúi ê lī)",
"form.prefs.label.display_mode": "Chiām-chìn sek bāng-lō͘ èng-iōng theng-sek (PWA) ê hián-sī bô͘-sek",
"form.prefs.label.entries_per_page": "Ta̍k ia̍h siau-sit sò͘",
"form.prefs.label.entry_order": "Siau-sit hián-sī sūn-sū ê i-kù",
"form.prefs.label.entry_sorting": "Siau-sit sūn-sū",
"form.prefs.label.entry_swipe": "Ē-sái tī chhiok-khòng sek êng-bō͘ ùi siau-sit iōng thoa tāng chhau-chok",
"form.prefs.label.external_font_hosts": "Gōa-pō͘ lī-hêng lâi-goân",
"form.prefs.label.gesture_nav": "Tī siau-sit kan sóa-ūi ê chhiú-sè",
"form.prefs.label.keyboard_shortcuts": "Ē-sái iōng khí-pôaⁿ khoài-sok khí",
"form.prefs.label.language": "Gú-giân",
"form.prefs.label.mark_read_manually": "Ka-kī chhau-chok kám beh chù chòe tha̍k kè",
"form.prefs.label.mark_read_on_media_completion": "Kan-na tī im-sìn, sī-sìn hòng-sàng kàu 90%% ê si-chun chù chòe tha̍k kè",
"form.prefs.label.mark_read_on_view": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè",
"form.prefs.label.mark_read_on_view_or_media_completion": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè, m̄-koh nā-sī im-sìn, sī-sìn tio̍h tī hòng-sàng kàu 90%% ê si-chun chiah lâi chù",
"form.prefs.label.media_playback_rate": "Im-sìn, sī-sìn pàng ê sok-tō͘",
"form.prefs.label.show_reading_time": "Hián-sī siau-sit àn-sǹg ài gōa-kú lâi tha̍k",
"form.prefs.label.theme": "Chú-tôe",
"form.prefs.label.timezone": "Sî-khu",
"form.prefs.select.alphabetical": "Chiàu lī-bú pâi",
"form.prefs.select.browser": "Tī iû-lám khì phah khui",
"form.prefs.select.created_time": "Siau-sit kiàn-li̍p sî-kan",
"form.prefs.select.fullscreen": "Choân êng-bō͘",
"form.prefs.select.minimal_ui": "Siōng iông-chhun--ê",
"form.prefs.select.none": "Bô",
"form.prefs.select.older_first": "Ùi kū--ê khai-sí pâi",
"form.prefs.select.publish_time": "Siau-sit hoat-pò͘ sî-kan",
"form.prefs.select.recent_first": "Ùi sin--ê khai-sí pâi",
"form.prefs.select.standalone": "To̍k-li̍p--ê",
"form.prefs.select.swipe": "Iōng thoa--ê",
"form.prefs.select.tap": "Tiám nn̄g pái",
"form.prefs.select.unread_count": "Ah-bōe tha̍k ê sò͘-liōng",
"form.submit.loading": "Tng leh chip-hêng…",
"form.submit.saving": "Tng leh pó-chûn…",
"form.user.label.admin": "Koán-lí-lâng",
"form.user.label.confirmation": "Koh su-li̍p chi̍t pái bi̍t-bé",
"form.user.label.password": "Bi̍t-bé",
"form.user.label.username": "Kháu-chō miâ",
"menu.about": "Iú-koan",
"menu.add_feed": "Sin cheng-ka siau-sit lâi-goân",
"menu.add_user": "Sin cheng-ka sú-iōng-lâng",
"menu.api_keys": "API só-sî",
"menu.categories": "Lūi-pia̍t",
"menu.create_api_key": "Sin cheng-ka chi̍t ê API só-sî",
"menu.create_category": "Sin cheng-ka lūi-pia̍t",
"menu.edit_category": "Pian-chi̍p",
"menu.edit_feed": "Pian-chi̍p",
"menu.export": "Hōe--chhut",
"menu.feed_entries": "Bûn-chiong",
"menu.feeds": "Siau-sit lâi-goân",
"menu.flush_history": "Hìⁿ-sak kì-lo̍k",
"menu.history": "Kì-lo̍k",
"menu.home_page": "Siú ia̍h",
"menu.import": "Hōe--li̍p",
"menu.integrations": "Chéng-ha̍p",
"menu.logout": "Teng-chhut",
"menu.mark_all_as_read": "Choân-pō͘ chù chòe tha̍k kè",
"menu.mark_page_as_read": "Kā chit ia̍h--ê lóng chù chòe tha̍k kè",
"menu.preferences": "Siat-tēng",
"menu.refresh_all_feeds": "Tī pōe-āu têng lia̍h só͘-ū ê siau-sit lâi-goân",
"menu.refresh_feed": "Têng lia̍h",
"menu.search": "Chhiau-chhē",
"menu.sessions": "Ū teng-lo̍k--ê",
"menu.settings": "Siat-tēng",
"menu.shared_entries": "Hun-hióng kè ê siau-sit",
"menu.show_all_entries": "Hián-sī só͘-ū ê siau-sit",
"menu.show_only_starred_entries": "Kan-na hián-sī siu-chông ê siau-sit",
"menu.show_only_unread_entries": "Kan-na hián-sī ah-bōe tha̍k kè ê siau-sit",
"menu.starred": "Siu-chông",
"menu.title": "Tō-lám",
"menu.unread": "Ah-bōe tha̍k",
"menu.users": "Sú-iōng-lâng",
"page.about.author": "Chok-chiá: ",
"page.about.build_date": "Kiàn-tì li̍t-kî:",
"page.about.credits": "Pán-koân",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Choân-he̍k siat-tēng soán-hāng",
"page.about.go_version": "Go pán-pún:",
"page.about.license": "Pàng-koân:",
"page.about.postgres_version": "Postgres pán-pún:",
"page.about.title": "Iú-koan",
"page.about.version": "Pán-pún:",
"page.add_feed.choose_feed": "Soán-te̍k chi̍t ê Siau-sit lâi-goân",
"page.add_feed.label.url": "Bāng-chí",
"page.add_feed.legend.advanced_options": "Chìn-kai soán-hāng",
"page.add_feed.no_category": "Ah bô lūi-pia̍t, chì-chió ài ū chi̍t ê",
"page.add_feed.submit": "Chhē Siau-sit lâi-goân",
"page.add_feed.title": "Sin cheng-ka Siau-sit lâi-goân",
"page.api_keys.never_used": "Bô iōng kè",
"page.api_keys.table.actions": "Chhau-chok",
"page.api_keys.table.created_at": "Kiàn-tì li̍t-kî",
"page.api_keys.table.description": "Biâu-su̍t",
"page.api_keys.table.last_used_at": "Siōng-bóe pái sú-iōng",
"page.api_keys.table.token": "Só-sî",
"page.api_keys.title": "API só-sî",
"page.categories_count": [
"%d ê lūi-pia̍t"
],
"page.categories.entries": "Siau-sit",
"page.categories.feed_count": [
"Ū %d ê Siau-sit lâi-goân"
],
"page.categories.feeds": "Siau-sit lâi-goân",
"page.categories.no_feed": "Ah-bô siau-sit lâi-goân",
"page.categories.title": "Lūi-pia̍t",
"page.category_label": "Lūi-pia̍t: %s",
"page.edit_category.title": "Pian-chi̍p lūi-pia̍t: %s",
"page.edit_feed.etag_header": "ETag piau-thâu:",
"page.edit_feed.last_check": "Siōng-bóe pái kiám-cha sî-kan",
"page.edit_feed.last_modified_header": "Siōng-bóe pái siu-kái piau-thâu:",
"page.edit_feed.last_parsing_error": "Siōng-bóe pái kái-sek m̄-tio̍h",
"page.edit_feed.no_header": "Bô",
"page.edit_feed.title": "Pian-chi̍p Siau-sit lâi-goân: %s",
"page.edit_user.title": "pian-chi̍p sú-iōng-lâng: %s",
"page.entry.attachments": "Hù-kiāⁿ",
"page.feeds.error_count": [
"%d ê m̄-tio̍h"
],
"page.feeds.last_check": "Siōng-bóe kiám-cha sî-kan:",
"page.feeds.next_check": "Āu-pái kiám-cha sî-kan:",
"page.feeds.read_counter": "Tha̍k kè--ê siau-sit sò͘",
"page.feeds.title": "Siau-sit lâi-goân",
"page.history.title": "Kì-lo̍k",
"page.import.title": "Hōe-li̍p",
"page.integration.bookmarklet": "Chheh-chhiam ke-si",
"page.integration.bookmarklet.help": "Lí ē-sái iōng chit ê te̍k-pia̍t ê chheh-chhiam ti̍t-chiap tēng bāng-ia̍h ê siau-sit",
"page.integration.bookmarklet.instructions": "Kā chit ê liân-kiat thoa khì iû-lám khì ê chheh-chhiam lân",
"page.integration.bookmarklet.name": "Siu-chông Miniflux",
"page.integration.miniflux_api": "Miniflux API",
"page.integration.miniflux_api_endpoint": "API thâu",
"page.integration.miniflux_api_password": "Bi̍t-bé",
"page.integration.miniflux_api_password_value": "Lí ê kháu-chō ê bi̍t-bé",
"page.integration.miniflux_api_username": "Kháu-chō miâ",
"page.integrations.title": "Chéng-ha̍p",
"page.keyboard_shortcuts.close_modal": "Kìm tiāu tùi-ōe thang",
"page.keyboard_shortcuts.download_content": "Liah goân-tóe ê siau-sit lōe-iông",
"page.keyboard_shortcuts.go_to_bottom_item": "Sóa khì thōng ē-kha ê siau-sit",
"page.keyboard_shortcuts.go_to_categories": "Phah khui lūi-pia̍t ia̍h",
"page.keyboard_shortcuts.go_to_feed": "Khì siau-sit lâi-goân",
"page.keyboard_shortcuts.go_to_feeds": "Phah khui siau-sit lâi-goân ia̍h",
"page.keyboard_shortcuts.go_to_history": "Phah khui kì-lo̍k ia̍h",
"page.keyboard_shortcuts.go_to_next_item": "Āu-chi̍t ê siau-sit",
"page.keyboard_shortcuts.go_to_next_page": "Āu-chi̍t ia̍h",
"page.keyboard_shortcuts.go_to_previous_item": "Téng-chi̍t ê siau-sit",
"page.keyboard_shortcuts.go_to_previous_page": "Téng-chi̍t ia̍h",
"page.keyboard_shortcuts.go_to_search": "Phah khui chhiau-chhē ia̍h",
"page.keyboard_shortcuts.go_to_settings": "Phah khui siat-tēng ia̍h",
"page.keyboard_shortcuts.go_to_starred": "Phah khui siu-chông--ê ia̍h",
"page.keyboard_shortcuts.go_to_top_item": "Sóa khì thōng téng-koân ê siau-sit",
"page.keyboard_shortcuts.go_to_unread": "Phah khui ah-bōe tha̍k--ê ia̍h",
"page.keyboard_shortcuts.mark_page_as_read": "Kā chit ia̍h--ê lóng chù chòe tha̍k--kè",
"page.keyboard_shortcuts.open_comments": "Phah khui hôe-èng liân-kiat",
"page.keyboard_shortcuts.open_comments_same_window": "Tī chit-má ê hun-ia̍h phah khui hôe-èng liân-kiat",
"page.keyboard_shortcuts.open_item": "Phah khui soán-te̍k ê siau-sit",
"page.keyboard_shortcuts.open_original": "Phah khui siau-sit goân-tóe ê liân-kiat",
"page.keyboard_shortcuts.open_original_same_window": "Tī chit-má ê hun-ia̍h phah khui siau-sit goân-tóe ê liân-kiat",
"page.keyboard_shortcuts.refresh_all_feeds": "Tī pōe-āu ōaⁿ-sin siau-sit lâi-goân",
"page.keyboard_shortcuts.remove_feed": "Thâi tiāu siau-sit lâi-goân",
"page.keyboard_shortcuts.save_article": "Pó-chûn siau-sit",
"page.keyboard_shortcuts.scroll_item_to_top": "Sóa khì bāng-ia̍h siōng téng-koân",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Hián-sī khoài-sok khí",
"page.keyboard_shortcuts.subtitle.actions": "Chhau-chok",
"page.keyboard_shortcuts.subtitle.items": "Bûn-chiong tō-lám",
"page.keyboard_shortcuts.subtitle.pages": "Ia̍h bīn tō-lám",
"page.keyboard_shortcuts.subtitle.sections": "Hun lân tō-lám",
"page.keyboard_shortcuts.title": "Khoài-sok khí",
"page.keyboard_shortcuts.toggle_bookmark_status": "Chhet-li̍p siu-chông chōng-thài",
"page.keyboard_shortcuts.toggle_entry_attachments": "Chhet-li̍p thián khui kah siu-ha̍p siau-sit hù-kiāⁿ ê chōng-thài",
"page.keyboard_shortcuts.toggle_read_status_next": "Chhet-li̍p tha̍k--kè, ah-bōe tha̍k ê chōng-thài, koh chiau-tiám tī āu-chi̍t--ê",
"page.keyboard_shortcuts.toggle_read_status_prev": "Chhet-li̍p tha̍k--kè, ah-bōe tha̍k ê chōng-thài, koh chiau-tiám tī téng-chi̍t--ê",
"page.login.google_signin": "Sú-iōng Google teng-lo̍k",
"page.login.oidc_signin": "Sú-iōng %s teng-lo̍k",
"page.login.title": "teng-lo̍k",
"page.login.webauthn_login": "Sú-iōng bi̍t-bé teng-lo̍k",
"page.login.webauthn_login.error": "Bô-hoat-tō͘ iōng bi̍t-bé teng-lo̍k",
"page.login.webauthn_login.help": "Sú-iōng an-choân só-sî teng-lo̍k ê sî-chūn, chhiáⁿ su-li̍p kháu-chō miâ. Nā-sī iōng thang chhiau-chhē ê Passkey (discoverable credentials) tio̍h bián.",
"page.new_api_key.title": "Sin ê API só-sî",
"page.new_category.title": "Sin lūi-pia̍t",
"page.new_user.title": "Sin sú-iōng-lâng",
"page.offline.message": "Lí í-keng lî-sòaⁿ",
"page.offline.refresh_page": "Chhì-khòaⁿ-māi têng tha̍k bāng-ia̍h",
"page.offline.title": "Lî-sòaⁿ bô͘-sek",
"page.read_entry_count": [
"%d ê tha̍k kè ê siau-sit"
],
"page.search.title": "Chhiau-chhē kiat-kó",
"page.sessions.table.actions": "Chhau-chok",
"page.sessions.table.current_session": "Chit-má teng-lo̍k--ê",
"page.sessions.table.date": "Li̍t-kî",
"page.sessions.table.ip": "IP tōe-chí",
"page.sessions.table.user_agent": "Sú-iōng-lâng tāi-lí",
"page.sessions.title": "Ū teng-lo̍k--ê",
"page.settings.link_google_account": "Kah góa ê Google kháu-chō kiat chòe-hé",
"page.settings.link_oidc_account": "Kah góa ê %s kháu-chō kiat chòe-hé",
"page.settings.title": "Siat-tēng",
"page.settings.unlink_google_account": "Phah khui kah góa ê Google kháu-chō ê kiat",
"page.settings.unlink_oidc_account": "Phah khui kah góa ê %s kháu-chō ê kiat",
"page.settings.webauthn.actions": "Chhau-chok",
"page.settings.webauthn.added_on": "Sin cheng-ka ê sî-kan",
"page.settings.webauthn.delete": [
"Thâi tiāu %d ê Passkey"
],
"page.settings.webauthn.last_seen_on": "Siōng-bóe pái sú-iōng sî-kan",
"page.settings.webauthn.passkey_name": "Passkey miâ",
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Chù-chheh Passkey",
"page.settings.webauthn.register.error": "Bô-hoat-tō͘ chù-chheh Passkey",
"page.shared_entries_count": [
"Í-keng hun-hióng %d ê siau-sit"
],
"page.shared_entries.title": "Hun-hióng kè ê siau-sit",
"page.starred_entry_count": [
"%d ê siu-chông ê siau-sit"
],
"page.starred.title": "Siu-chông",
"page.total_entry_count": [
"Lóng-chóng %d ê siau-sit"
],
"page.unread_entry_count": [
"%d ê siau-sit ah-bōe tha̍k"
],
"page.unread.title": "Ah-bōe tha̍k",
"page.users.actions": "chhau-chok",
"page.users.admin.no": "Hóⁿ",
"page.users.admin.yes": "Sī",
"page.users.is_admin": "Koán-lí-lâng",
"page.users.last_login": "Siōng-bóe pái teng-lo̍k",
"page.users.never_logged": "Chū-lâi bô teng-lo̍k kè",
"page.users.title": "Sú-iōng-lâng",
"page.users.username": "Sú-iōng-lâng miâ",
"page.webauthn_rename.title": "Tiông-sin hō͘ miâ Passkey",
"pagination.first": "Thâu-chi̍t ia̍h",
"pagination.last": "Siōng-bóe ia̍h",
"pagination.next": "Āu-chi̍t ia̍h",
"pagination.previous": "Téng-chi̍t ia̍h",
"search.label": "Chhiau-chhē",
"search.placeholder": "Chhiau-chhē...",
"search.submit": "Chhiau-chhē",
"skip_to_content": "Sóa kah chú-iàu ê lōe-iông",
"time_elapsed.days": [
"%d kang chêng"
],
"time_elapsed.hours": [
"%d tiám-cheng chêng"
],
"time_elapsed.minutes": [
"%d hun-cheng chêng"
],
"time_elapsed.months": [
"%d kò ge̍h chêng"
],
"time_elapsed.not_yet": "ah-bōe",
"time_elapsed.now": "tú-chiah",
"time_elapsed.weeks": [
"%d lé-pài chêng"
],
"time_elapsed.years": [
"%d nî chêng"
],
"time_elapsed.yesterday": "cha-hng",
"tooltip.keyboard_shortcuts": "Khoài-sok khí:%s",
"tooltip.logged_user": "Chit-má teng-lo̍k--ê: %s"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+626
View File
@@ -0,0 +1,626 @@
{
"action.cancel": "abandon",
"action.download": "Descărcare",
"action.edit": "Editare",
"action.home_screen": "Adaugă pe ecranul principal",
"action.import": "Importă",
"action.login": "Autentificare",
"action.or": "sau",
"action.remove": "Elimină",
"action.remove_feed": "Elimină acest flux",
"action.save": "Salvează",
"action.subscribe": "Abonează-te",
"action.update": "Actualizare",
"alert.account_linked": "Contul dvs. extern este atașat!",
"alert.account_unlinked": "Am decuplat contul dvs. extern!",
"alert.background_feed_refresh": "Toate fluxurile sunt actualizate în fundal. Puteți să continuați utilizarea Miniflux în timp ce procesul rulează.",
"alert.feed_error": "Este o problemă cu acest flux",
"alert.no_bookmark": "Nu sunt înregistrări marcate.",
"alert.no_category": "Nu sunt categorii.",
"alert.no_category_entry": "Nu sunt înregistrări în această categorie.",
"alert.no_feed": "Nu aveți fluxuri.",
"alert.no_feed_entry": "Nu sunt înregistrări pentru acest flux.",
"alert.no_feed_in_category": "Nu sunt fluxuri pentru această categorie.",
"alert.no_history": "Nu există istoric în acest moment.",
"alert.no_search_result": "Nu există înregistrări pentru această căutare.",
"alert.no_shared_entry": "Nu sunt înregistrări partajate.",
"alert.no_tag_entry": "Nu sunt înregistrări pentru această etichetă.",
"alert.no_unread_entry": "Nu sunt intrări necitite.",
"alert.no_user": "Sunteți singurul utilizator.",
"alert.pocket_linked": "Contul dvs. Pocket este atașat!",
"alert.prefs_saved": "Preferințe salvate!",
"alert.too_many_feeds_refresh": [
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minut înainte de a reîncerca.",
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minute înainte de a reîncerca.",
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minute înainte de a reîncerca."
],
"confirm.loading": "În progres…",
"confirm.no": "nu",
"confirm.question": "Suneți sigur?",
"confirm.question.refresh": "Sunteți sigur că vreți să forțați reîmprospătarea?",
"confirm.yes": "da",
"enclosure_media_controls.seek": "Caută:",
"enclosure_media_controls.seek.title": "Caută %s secunde",
"enclosure_media_controls.speed": "Viteză:",
"enclosure_media_controls.speed.faster": "Mai rapid",
"enclosure_media_controls.speed.faster.title": "Mai rapid cu %sx",
"enclosure_media_controls.speed.reset": "Resetare",
"enclosure_media_controls.speed.reset.title": "Resetare viteză la 1x",
"enclosure_media_controls.speed.slower": "Mai încet",
"enclosure_media_controls.speed.slower.title": "Mai încet cu %sx",
"entry.bookmark.toast.off": "Fără stea",
"entry.bookmark.toast.on": "Cu stea",
"entry.bookmark.toggle.off": "Fără stea",
"entry.bookmark.toggle.on": "Stea",
"entry.comments.label": "Comentarii",
"entry.comments.title": "Vizualizare Comentarii",
"entry.estimated_reading_time": [
"%d minut de lectură",
"%d minute de lectură",
"%d minut de lectură"
],
"entry.external_link.label": "Legătură externă",
"entry.save.completed": "Gata!",
"entry.save.label": "Salvare",
"entry.save.title": "Salvez această înregistrare",
"entry.save.toast.completed": "Înregistrare salvată",
"entry.scraper.completed": "Gata!",
"entry.scraper.label": "Descărcare",
"entry.scraper.title": "Descarcă conținutul original",
"entry.share.label": "Partajare",
"entry.share.title": "Partajează această înregistrare",
"entry.shared_entry.label": "Partajare",
"entry.shared_entry.title": "Deschide legătura publică",
"entry.state.loading": "Încarc…",
"entry.state.saving": "Salvez…",
"entry.status.mark_as_read": "Marcați ca citit",
"entry.status.mark_as_unread": "Marcați ca necitit",
"entry.status.title": "Modifică starea intrării",
"entry.status.toast.read": "Marcat ca citit",
"entry.status.toast.unread": "Marcat ca necitit",
"entry.tags.label": "Etichete:",
"entry.unshare.label": "Elimină partajarea",
"error.api_key_already_exists": "Această cheie API există deja.",
"error.bad_credentials": "Utilizator sau parolă invalide.",
"error.category_already_exists": "Această categorie există deja.",
"error.category_not_found": "Această categorie nu există sau nu aparține acestui utilizator.",
"error.database_error": "Eroare bază de date: %v.",
"error.different_passwords": "Parolele nu sunt identice.",
"error.duplicate_fever_username": "Este deja cineva cu același cont de Fever!",
"error.duplicate_googlereader_username": "Este deja cineva cu același nume de utilizator Google Reader!",
"error.duplicate_linked_account": "Este deja cineva asociat cu acest furnizor!",
"error.duplicated_feed": "Acest flux există deja.",
"error.empty_file": "Acest fișier este gol.",
"error.entries_per_page_invalid": "Numărul de înregistrări de pe pagină nu este valid.",
"error.feed_already_exists": "Acest flux există deja.",
"error.feed_category_not_found": "Această categorie nu există sau nu aparține utilizatorului.",
"error.feed_format_not_detected": "Nu pot detecta formatul fluxului: %v.",
"error.feed_invalid_blocklist_rule": "Blocul listei de reguli este invalid.",
"error.feed_invalid_keeplist_rule": "Lista de reguli keep este invalidă.",
"error.feed_mandatory_fields": "Adresa URL și categoria sunt obligatorii.",
"error.feed_not_found": "Acest flux nu există sau un aparține acestui utilizator.",
"error.feed_title_not_empty": "Titlul fluxului nu poate fi gol.",
"error.feed_url_not_empty": "Adresa URL a fluxului nu poate fi goală.",
"error.fields_mandatory": "Toate câmpurile sunt obligatorii.",
"error.http_bad_gateway": "Acest site web nu este disponibil momentan din cauza unei erori generată de gateway. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_body_read": "Nu pot citi corpul HTTP: %v.",
"error.http_client_error": "Eroare client HTTP: %v.",
"error.http_empty_response": "Răspunsul HTTP este gol. Poate acest site web utilizează un mecanism împotriva boților?",
"error.http_empty_response_body": "Corpul răspunsului HTTP este gol.",
"error.http_forbidden": "Accesul la acest site web este interzis. Poate acesta utilizează un mecanism împotriva boților?",
"error.http_gateway_timeout": "Acest site web nu este disponibil momentan din cauza unei erori generată de gateway. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_internal_server_error": "Acest site web nu este disponibil momentan din cauza unei erori generată de server. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_not_authorized": "Accesul la acest site nu este autorizat. Poate fi din cauza parolei sau a userului greșite.",
"error.http_resource_not_found": "Resursa solicitată nu este găsită. Vă rog să verificați URL-ul.",
"error.http_response_too_large": "Răspunsul HTTP este prea mare. Puteți crește dimensiunea acestuia în setările globale (necesită repornirea server-ului).",
"error.http_service_unavailable": "Acest site web nu este disponibil momentan din cauza unei erori generată de server. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_too_many_requests": "Miniflux a generat prea multe solicitări pe acest site web. Vă rog, încercați mai tîrziu sau modificați configurațiile aplicației.",
"error.http_unexpected_status_code": "Acest site web nu este disponibil momentan din cauza unei erori HTTP: %d. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.invalid_default_home_page": "Pagină de start invalidă!",
"error.invalid_display_mode": "Mod invalid de afișare în aplicația web.",
"error.invalid_entry_direction": "Direcție invalidă ăn intrare.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Adresa URL a fluxului este invalidă.",
"error.invalid_gesture_nav": "Gest de navigare invalid.",
"error.invalid_language": "Limbă invalidă.",
"error.invalid_site_url": "Adresa URL a site-ului este invalidă.",
"error.invalid_theme": "Temă invalidă.",
"error.invalid_timezone": "Dată/oră invalide.",
"error.network_operation": "Miniflux nu poate ajunge la acest site din cauza unei erori de rețea: %v.",
"error.network_timeout": "Acest site web este prea lent și conexiunea nu s-a realizat: %v",
"error.password_min_length": "Parola trebuie să aibă cel puțin 6 caractere.",
"error.pocket_access_token": "Nu poate obține token-ul de acces de la Pocket!",
"error.pocket_request_token": "Nu poate obține token-ul solicitat de la Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Regulă de bloc invalidă: regulii #%d îi lipsește un nume valid de câmp (Opțiuni: %s)",
"error.settings_block_rule_invalid_regex": "Regulă de bloc invalidă: modelul regulii #%d's nu este regex valid",
"error.settings_block_rule_regex_required": "Regulă de bloc invalidă: modelul regulii #%d's nu este furnizat",
"error.settings_block_rule_separator_required": "Regulă de bloc invalidă: modelul regulii #%d's trebuie separat de '='",
"error.settings_invalid_domain_list": "Lista domeniilor invalidă. Vă rugăm să ne furnizați o listă separată de virgulă a domeniilor.",
"error.settings_keep_rule_fieldname_invalid": "Regulă Keep invalidă: regulii #%d îi lipsește un nume valid (Opțiuni: %s)",
"error.settings_keep_rule_invalid_regex": "Regulă Keep invalidă: modelul regulii #%d's nu este regex valid",
"error.settings_keep_rule_regex_required": "Regulă Keep invalidă: modelul regulii #%d nu este furnizat",
"error.settings_keep_rule_separator_required": "Regulă Keep invalidă: modelul regulii #%d's trebuie separat de'='",
"error.settings_mandatory_fields": "Numele utilizatorului, tema, limba și fusul orar sunt obligatorii.",
"error.settings_media_playback_rate_range": "Viteza de rulare nu este validă",
"error.settings_reading_speed_is_positive": "Vitezele de citire trebuie să fie numere întregi pozitive.",
"error.site_url_not_empty": "Adresa URL a site-ului nu poate fi goală.",
"error.subscription_not_found": "Nu se poate găsi nici un flux.",
"error.title_required": "Titlul este obligatoriu.",
"error.tls_error": "Eroare TLS: %q. Puteți dezactiva verificarea TLS în setările fluxurilor dacă doriți.",
"error.unable_to_create_api_key": "Nu pot crea această cheie API.",
"error.unable_to_create_category": "Nu se poate crea această categorie.",
"error.unable_to_create_user": "Nu se poate crea utilizatorul.",
"error.unable_to_detect_rssbridge": "Nu pot detecta fluxul când utilizez RSS-Bridge: %v.",
"error.unable_to_parse_feed": "Nu pot procesa acest flux: %v.",
"error.unable_to_update_category": "Nu se poate actualiza această categorie.",
"error.unable_to_update_feed": "Nu se poate actualiza acest flux.",
"error.unable_to_update_user": "Nu se poate actualiza utilizatorul.",
"error.unlink_account_without_password": "Trebuie să definiți o parolă, altfel nu vă veți mai putea conecta.",
"error.user_already_exists": "Acest utilizator există deja.",
"error.user_mandatory_fields": "Numele utilizatorului este obligatoriu.",
"form.api_key.label.description": "Etichetă Cheie API",
"form.category.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.category.label.title": "Titlu",
"form.feed.fieldset.general": "General",
"form.feed.fieldset.integration": "Servicii Terțe",
"form.feed.fieldset.network_settings": "Setări Rețea",
"form.feed.fieldset.rules": "Reguli",
"form.feed.label.allow_self_signed_certificates": "Permite certificatele auto-semnate sau invalide",
"form.feed.label.apprise_service_urls": "Lista de URL-uri ale serviciilor Apprise separate prin virgule",
"form.feed.label.blocklist_rules": "Reguli de Blocare",
"form.feed.label.category": "Categorie",
"form.feed.label.cookie": "Setare Cookie-uri",
"form.feed.label.crawler": "Aduce conținutul original",
"form.feed.label.description": "Descriere",
"form.feed.label.disable_http2": "Dezactivează HTTP/2 pentru a preveni amprentarea",
"form.feed.label.disabled": "Nu actualiza acest flux",
"form.feed.label.feed_password": "Parolă Flux",
"form.feed.label.feed_url": "Flux URL",
"form.feed.label.feed_username": "Nume user Flux",
"form.feed.label.fetch_via_proxy": "Utilizați proxy-ul configurat la nivelul aplicației",
"form.feed.label.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.feed.label.ignore_http_cache": "Ignoră cache HTTP",
"form.feed.label.keeplist_rules": "Reguli de Păstrare",
"form.feed.label.no_media_player": "Nu există player media (audio/video)",
"form.feed.label.ntfy_activate": "Împinge intrările la ntfy",
"form.feed.label.ntfy_default_priority": "Prioritate predefinită Ntfy",
"form.feed.label.ntfy_high_priority": "Prioritate ridicată Ntfy",
"form.feed.label.ntfy_low_priority": "Prioritate redusă Ntfy",
"form.feed.label.ntfy_max_priority": "Prioritate maximă Ntfy",
"form.feed.label.ntfy_min_priority": "Prioritate minimă Ntfy",
"form.feed.label.ntfy_priority": "Prioritate Ntfy",
"form.feed.label.ntfy_topic": "Subiect Ntfy (opțional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Activează Pushover",
"form.feed.label.pushover_default_priority": "Prioritate implicită Pushover",
"form.feed.label.pushover_high_priority": "Prioritate ridicată Pushover",
"form.feed.label.pushover_low_priority": "Prioritate redusă Pushover",
"form.feed.label.pushover_max_priority": "Prioritate maximă Pushover",
"form.feed.label.pushover_min_priority": "Prioritate minimă Pushover",
"form.feed.label.pushover_priority": "Prioritate Pushover",
"form.feed.label.rewrite_rules": "Rescrie Regulile",
"form.feed.label.scraper_rules": "Reguli de Eliminare",
"form.feed.label.site_url": "Adresă URL",
"form.feed.label.title": "Titlu",
"form.feed.label.urlrewrite_rules": "URL Reguli de Rescriere",
"form.feed.label.user_agent": "Suprascrie User Agent Predefinit",
"form.feed.label.webhook_url": "URL Webhook (pentru a primi notificări despre evenimentele de intrare)",
"form.import.label.file": "Fișier OPML",
"form.import.label.url": "URL",
"form.integration.apprise_activate": "Trimite înregistrările pe Apprise",
"form.integration.apprise_services_url": "URL-uri separate de virgulă cu servicii Apprise",
"form.integration.apprise_url": "URL API Apprise",
"form.integration.betula_activate": "Salvează înregistrările în Betula",
"form.integration.betula_token": "Token Betula",
"form.integration.betula_url": "Adresă server Betula",
"form.integration.cubox_activate": "Salvează intrările în Cubox",
"form.integration.cubox_api_link": "Link APi Cubox",
"form.integration.discord_activate": "Împinge intrările pe Discord",
"form.integration.discord_webhook_link": "Link Webhook Discord",
"form.integration.espial_activate": "Save entries to Espial",
"form.integration.espial_api_key": "Cheie API Espial",
"form.integration.espial_endpoint": "Punct acces API Espial",
"form.integration.espial_tags": "Etichete Espial",
"form.integration.fever_activate": "Activează API Fever",
"form.integration.fever_endpoint": "Punct access API Fever:",
"form.integration.fever_password": "Parolă Fever",
"form.integration.fever_username": "Utilizator Fever",
"form.integration.googlereader_activate": "Activează API Google Reader",
"form.integration.googlereader_endpoint": "Punct acces API Google Reader:",
"form.integration.googlereader_password": "Parolă Google Reader",
"form.integration.googlereader_username": "Utilizator Google Reader",
"form.integration.instapaper_activate": "Salvează înregistrările pe Instapaper",
"form.integration.instapaper_password": "Parolă Instapaper",
"form.integration.instapaper_username": "Utilizator Instapaper",
"form.integration.linkace_activate": "Salvează intrările în LinkAce",
"form.integration.linkace_api_key": "Cheie API LinkAce",
"form.integration.linkace_check_disabled": "Dezactivează verificarea link-urilor",
"form.integration.linkace_endpoint": "Endpoint API LinkAce",
"form.integration.linkace_is_private": "Marchează link-urile ca private",
"form.integration.linkace_tags": "Tag-uri LinkAce",
"form.integration.linkding_activate": "Salvează intrările în Linkding",
"form.integration.linkding_api_key": "Cheie API Linkding",
"form.integration.linkding_bookmark": "Marchează semnele de carte ca necitite",
"form.integration.linkding_endpoint": "Endpoint API Linkding",
"form.integration.linkding_tags": "TAG-uri Linkding",
"form.integration.linkwarden_activate": "Salvează intrările în Linkwarden",
"form.integration.linkwarden_api_key": "Cheie API Linkwarden",
"form.integration.linkwarden_endpoint": "Endpoint API Linkwarden",
"form.integration.matrix_bot_activate": "Împinge intrările noi pe Matrix",
"form.integration.matrix_bot_chat_id": "ID-ul Camerei Matrix",
"form.integration.matrix_bot_password": "Parola utilizatorului Matrix",
"form.integration.matrix_bot_url": "Server URL Matrix",
"form.integration.matrix_bot_user": "Utilizator Matrix",
"form.integration.notion_activate": "Salvează înregistrările în Notion",
"form.integration.notion_page_id": "ID Pagină Notion",
"form.integration.notion_token": "Token Secret Notion",
"form.integration.ntfy_activate": "Împinge intrările pe ntfy",
"form.integration.ntfy_api_token": "Token API Ntfy (opțional)",
"form.integration.ntfy_icon_url": "Icon URL Ntfy (opțional)",
"form.integration.ntfy_internal_links": "Utilizează legături interne la clic (opțional)",
"form.integration.ntfy_password": "Parolă Ntfy (opțional)",
"form.integration.ntfy_topic": "Topic Ntfy",
"form.integration.ntfy_url": "URL Ntfy (opțional, predefinit este ntfy.sh)",
"form.integration.ntfy_username": "Utilizator Ntfy (opțional)",
"form.integration.nunux_keeper_activate": "Salvează înregistrările în Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Cheie API Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Punct de acces API Keeper",
"form.integration.omnivore_activate": "Salvare înregistrări în Omnivore",
"form.integration.omnivore_api_key": "Cheie API Omnivore",
"form.integration.omnivore_url": "Punct acces API Omnivore",
"form.integration.pinboard_activate": "Salvează intrările în Pinboard",
"form.integration.pinboard_bookmark": "Marchează bookmark ca necitit",
"form.integration.pinboard_tags": "Etichete Pinboard",
"form.integration.pinboard_token": "Token API Pinboard",
"form.integration.pocket_access_token": "Token Acces Pocket",
"form.integration.pocket_activate": "Salvează înregistrările în Pocket",
"form.integration.pocket_connect_link": "Conectează contul Pocket personal",
"form.integration.pocket_consumer_key": "Cheie Consumator Pocket",
"form.integration.pushover_activate": "Activează Pushover",
"form.integration.pushover_device": "Dispozitiv Pushover (opțional)",
"form.integration.pushover_prefix": "Prefix Pushover (opțional)",
"form.integration.pushover_token": "Token Pushover",
"form.integration.pushover_user": "Utilizator Pushover",
"form.integration.raindrop_activate": "Salvează intrările în Raindrop",
"form.integration.raindrop_collection_id": "ID Colecție",
"form.integration.raindrop_tags": "Tag-uri (separate de virgulă)",
"form.integration.raindrop_token": "(Test) Token",
"form.integration.readeck_activate": "Salvează intrările în readeck",
"form.integration.readeck_api_key": "Cheie API Readeck",
"form.integration.readeck_endpoint": "URL Readeck",
"form.integration.readeck_labels": "Etichete Readeck",
"form.integration.readeck_only_url": "Trimite numai URL (în loc de tot conținutul)",
"form.integration.readwise_activate": "Salvare înregistrări în Readwise Reader",
"form.integration.readwise_api_key": "Token Acces Readwise Reader",
"form.integration.readwise_api_key_link": "Obțineți Token-ul de Acess pe Readwise",
"form.integration.rssbridge_activate": "Verifică RSS-Bridge la adăugarea de abonamente",
"form.integration.rssbridge_url": "URL server RSS-Bridge",
"form.integration.shaarli_activate": "Salvează articolele în Shaarli",
"form.integration.shaarli_api_secret": "Secret API Shaarli",
"form.integration.shaarli_endpoint": "URL Shaarli",
"form.integration.shiori_activate": "Salvează articolele în Shiori",
"form.integration.shiori_endpoint": "Endpoint API Shiori",
"form.integration.shiori_password": "Parolă Shiori",
"form.integration.shiori_username": "Utilizator Shiori",
"form.integration.slack_activate": "Împinge intrările pe Slack",
"form.integration.slack_webhook_link": "Link Webhook Slack",
"form.integration.telegram_bot_activate": "Împingeți înregistrările noi pe chat-ul Telegram",
"form.integration.telegram_bot_disable_buttons": "Dezactivează butoanele",
"form.integration.telegram_bot_disable_notification": "Dezactivează notificările",
"form.integration.telegram_bot_disable_web_page_preview": "Dezactivează previzualizarea paginii web",
"form.integration.telegram_bot_token": "Token Bot",
"form.integration.telegram_chat_id": "ID Chat",
"form.integration.telegram_topic_id": "ID Topic",
"form.integration.wallabag_activate": "Salvează înregistrările în Wallabag",
"form.integration.wallabag_client_id": "ID Client Wallabag",
"form.integration.wallabag_client_secret": "Secret Client Wallabag",
"form.integration.wallabag_endpoint": "URL Wallabag",
"form.integration.wallabag_only_url": "Trimite numai URL-ul (fără conținut complet)",
"form.integration.wallabag_password": "Parolă Wallabag",
"form.integration.wallabag_username": "Utilizator Wallabag",
"form.integration.webhook_activate": "Activează Webhook",
"form.integration.webhook_secret": "Secret Webhook",
"form.integration.webhook_url": "URL Webhook",
"form.prefs.fieldset.application_settings": "Setări Aplicație",
"form.prefs.fieldset.authentication_settings": "Setări Autentificare",
"form.prefs.fieldset.global_feed_settings": "Setări Globale pt. Flux",
"form.prefs.fieldset.reader_settings": "Setări Citire",
"form.prefs.help.external_font_hosts": "Lista fonturilor de pe gazdă separate de virgulă care poate fi utilizate. De exemplu: \"fonts.gstatic.com fonts.googleapis.com\".",
"form.prefs.label.categories_sorting_order": "Sortare categorii",
"form.prefs.label.cjk_reading_speed": "Viteză de citire pentru Chineză, Coreană și Japoneză (caractere pe minut)",
"form.prefs.label.custom_css": "CSS personalizat",
"form.prefs.label.custom_js": "JavaScript personalizat",
"form.prefs.label.default_home_page": "Pagina pornire predefinită",
"form.prefs.label.default_reading_speed": "Viteză de citire pentru alte limbi (cuvinte pe minut)",
"form.prefs.label.display_mode": "Mod afișare Aplicație Web Progresivă (PWA)",
"form.prefs.label.entries_per_page": "Intrări pe pagină",
"form.prefs.label.entry_order": "Coloană de sortare",
"form.prefs.label.entry_sorting": "Sortare intrări",
"form.prefs.label.entry_swipe": "Activare glisare pentru ecranele tactile",
"form.prefs.label.external_font_hosts": "Fonturi externe gazdă",
"form.prefs.label.gesture_nav": "Gesturi pentru navigare între înregistrări",
"form.prefs.label.keyboard_shortcuts": "Activare scurtături tastatură",
"form.prefs.label.language": "Limbă",
"form.prefs.label.mark_read_manually": "Marchează manual intrările ca citite",
"form.prefs.label.mark_read_on_media_completion": "Marchează ca citit numai când redarea de conținut audio/video atinge 90%%",
"form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut",
"form.prefs.label.media_playback_rate": "Viteza de rulare audio/video",
"form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
"form.prefs.label.theme": "Temă",
"form.prefs.label.timezone": "Fus orar",
"form.prefs.select.alphabetical": "Alfabetic",
"form.prefs.select.browser": "Browser",
"form.prefs.select.created_time": "Dată creare înregistrare",
"form.prefs.select.fullscreen": "Ecran complet",
"form.prefs.select.minimal_ui": "Minim",
"form.prefs.select.none": "Nimic",
"form.prefs.select.older_first": "Intrările mai vechi la început",
"form.prefs.select.publish_time": "Data publicare înregistrare",
"form.prefs.select.recent_first": "Intrările mai noi la început",
"form.prefs.select.standalone": "Independent",
"form.prefs.select.swipe": "Glisare",
"form.prefs.select.tap": "Apăsare dublă",
"form.prefs.select.unread_count": "Contor necitite",
"form.submit.loading": "Încarc…",
"form.submit.saving": "Salvez…",
"form.user.label.admin": "Administrator",
"form.user.label.confirmation": "Confirmare Parolă",
"form.user.label.password": "Parolă",
"form.user.label.username": "Nume utilizator",
"menu.about": "Despre",
"menu.add_feed": "Adaugă flux",
"menu.add_user": "Adaugă utilizator",
"menu.api_keys": "Chei API",
"menu.categories": "Categorii",
"menu.create_api_key": "Crează o nouă cheie API",
"menu.create_category": "Crează o categorie",
"menu.edit_category": "Editare",
"menu.edit_feed": "Editare",
"menu.export": "Exportă",
"menu.feed_entries": "Intrări",
"menu.feeds": "Fluxuri",
"menu.flush_history": "Elimină istoricul",
"menu.history": "Istoric",
"menu.home_page": "Pagina principală",
"menu.import": "Importă",
"menu.integrations": "Integrări",
"menu.logout": "Deconectare",
"menu.mark_all_as_read": "Marchează tot ca citit",
"menu.mark_page_as_read": "Marchează această pagină ca citită",
"menu.preferences": "Preferințe",
"menu.refresh_all_feeds": "Reînnoiește toate fluxurile în fundal",
"menu.refresh_feed": "Reînnoire",
"menu.search": "Caută",
"menu.sessions": "Sesiuni",
"menu.settings": "Setări",
"menu.shared_entries": "Intrări partajate",
"menu.show_all_entries": "Afișează toate intrările",
"menu.show_only_starred_entries": "Afișează numai intrările marcate",
"menu.show_only_unread_entries": "Afișează numai intrările necitite",
"menu.starred": "Marcat",
"menu.title": "Meniu",
"menu.unread": "Necitit",
"menu.users": "Utilizatori",
"page.about.author": "Autor:",
"page.about.build_date": "Dată Build:",
"page.about.credits": "Credit",
"page.about.db_usage": "Utilizare Bază de Date",
"page.about.global_config_options": "Opțiuni globale de configurare",
"page.about.go_version": "Versiune Go:",
"page.about.license": "Licență:",
"page.about.postgres_version": "Versiune Postgres:",
"page.about.title": "Despre",
"page.about.version": "Versiune:",
"page.add_feed.choose_feed": "Alegeți un flux",
"page.add_feed.label.url": "URL",
"page.add_feed.legend.advanced_options": "Opțiuni Avansate",
"page.add_feed.no_category": "Nu există categorii. Trebuie să aveți măcar o categorie.",
"page.add_feed.submit": "Găsește un flux",
"page.add_feed.title": "Flux nou",
"page.api_keys.never_used": "Niciodată Utilizată",
"page.api_keys.table.actions": "Acțiuni",
"page.api_keys.table.created_at": "Dată Creare",
"page.api_keys.table.description": "Descriere",
"page.api_keys.table.last_used_at": "Utilizat ultima dată",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Chei API",
"page.categories_count": [
"%d categorie",
"%d categorii",
"%d categorie găsită"
],
"page.categories.entries": "Intrări",
"page.categories.feed_count": [
"Este %d flux.",
"Sunt %d fluxuri.",
"Sunt %d fluxuri găsite."
],
"page.categories.feeds": "Fluxuri",
"page.categories.no_feed": "Nici un flux.",
"page.categories.title": "Categorii",
"page.category_label": "Categorie: %s",
"page.edit_category.title": "Editare Categorie: %s",
"page.edit_feed.etag_header": "Antet ETag:",
"page.edit_feed.last_check": "Ultima verificare:",
"page.edit_feed.last_modified_header": "UltimaModificare antet:",
"page.edit_feed.last_parsing_error": "Ultima Eroare la Analiză",
"page.edit_feed.no_header": "Nimic",
"page.edit_feed.title": "Editare Flux: %s",
"page.edit_user.title": "Editare Utilizator: %s",
"page.entry.attachments": "Atașamente",
"page.feeds.error_count": [
"%d eroare",
"%d erori",
"%d erori găsite"
],
"page.feeds.last_check": "Ultima verificare:",
"page.feeds.next_check": "Următoarea verificare:",
"page.feeds.read_counter": "Numărul de intrări citite",
"page.feeds.title": "Fluxuri",
"page.history.title": "Istoric",
"page.import.title": "Import",
"page.integration.bookmarklet": "Marcaje",
"page.integration.bookmarklet.help": "Această legătură specială permite să vă abonați direct pe un site prin utilizarea unui marcaj în browser-ul web.",
"page.integration.bookmarklet.instructions": "Trageți legătura în favorite.",
"page.integration.bookmarklet.name": "Adaugă în Miniflux",
"page.integration.miniflux_api": "API Miniflux",
"page.integration.miniflux_api_endpoint": "Punct de acces API",
"page.integration.miniflux_api_password": "Parolă",
"page.integration.miniflux_api_password_value": "Parola contului",
"page.integration.miniflux_api_username": "Utilizator",
"page.integrations.title": "Integrări",
"page.keyboard_shortcuts.close_modal": "Închide fereastra de dialog",
"page.keyboard_shortcuts.download_content": "Descarcă conținutul original",
"page.keyboard_shortcuts.go_to_bottom_item": "Du-te la ultimul obiect",
"page.keyboard_shortcuts.go_to_categories": "Du-te la categorii",
"page.keyboard_shortcuts.go_to_feed": "Du-te la flux",
"page.keyboard_shortcuts.go_to_feeds": "Du-te la fluxuri",
"page.keyboard_shortcuts.go_to_history": "Du-te la istoric",
"page.keyboard_shortcuts.go_to_next_item": "Du-te la obiectul următor",
"page.keyboard_shortcuts.go_to_next_page": "Du-te la pagina următoare",
"page.keyboard_shortcuts.go_to_previous_item": "Du-te la obiectul anterior",
"page.keyboard_shortcuts.go_to_previous_page": "Du-te la pagina anterioară",
"page.keyboard_shortcuts.go_to_search": "Focusul pe formularul de căutare",
"page.keyboard_shortcuts.go_to_settings": "Du-te la setări",
"page.keyboard_shortcuts.go_to_starred": "Du-te la marcat",
"page.keyboard_shortcuts.go_to_top_item": "Du-te la primul obiect",
"page.keyboard_shortcuts.go_to_unread": "Du-te la necitit",
"page.keyboard_shortcuts.mark_page_as_read": "Marchează pagina curentă ca citită",
"page.keyboard_shortcuts.open_comments": "Deschide comentariile link-ului",
"page.keyboard_shortcuts.open_comments_same_window": "Deschide comentariile link-ului în tab-ul curent",
"page.keyboard_shortcuts.open_item": "Deschide obiectul selectat",
"page.keyboard_shortcuts.open_original": "Deschide link-ul original",
"page.keyboard_shortcuts.open_original_same_window": "Deschide link-ul original în tab-ul curent",
"page.keyboard_shortcuts.refresh_all_feeds": "Reîncarcă toate fluxurile în fundal",
"page.keyboard_shortcuts.remove_feed": "Elimină acest flux",
"page.keyboard_shortcuts.save_article": "Salvare înregistrare",
"page.keyboard_shortcuts.scroll_item_to_top": "Derulează obiectul la început",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Afișează scurtăturile tastaturii",
"page.keyboard_shortcuts.subtitle.actions": "Acțiuni",
"page.keyboard_shortcuts.subtitle.items": "Navigare Obiecte",
"page.keyboard_shortcuts.subtitle.pages": "Navigare Pagini",
"page.keyboard_shortcuts.subtitle.sections": "Navigare Secțiuni",
"page.keyboard_shortcuts.title": "Scurtături Tastatură",
"page.keyboard_shortcuts.toggle_bookmark_status": "Comută marcate",
"page.keyboard_shortcuts.toggle_entry_attachments": "Comută deschis/închis pe atașamentele înregistrării",
"page.keyboard_shortcuts.toggle_read_status_next": "Comută citit/necitit focus următor",
"page.keyboard_shortcuts.toggle_read_status_prev": "Comută citit/necitit, focus anterior",
"page.login.google_signin": "Conectare cu Google",
"page.login.oidc_signin": "Conectare cu %s",
"page.login.title": "Conectare",
"page.login.webauthn_login": "Conectare cu cheia de acces",
"page.login.webauthn_login.error": "Eroare la conectarea cu cheia de acces",
"page.login.webauthn_login.help": "Vă rog să introduceți numele utilizatorului dacă utilizați o cheie. Nu este necesară dacă utilizați o cheie de acces (credențiale descoperibile).",
"page.new_api_key.title": "Cheie API Nouă",
"page.new_category.title": "Categorie Nouă",
"page.new_user.title": "Utilizator Nou",
"page.offline.message": "Sunteți offline",
"page.offline.refresh_page": "Încercați să reîmprospătați pagina",
"page.offline.title": "Mod Offline",
"page.read_entry_count": [
"%d înregistrare citită",
"%d înregistrări citite",
"%d înregistrări citite"
],
"page.search.title": "Rezultate Căutare",
"page.sessions.table.actions": "Acțiuni",
"page.sessions.table.current_session": "Sesiunea Curentă",
"page.sessions.table.date": "Dată",
"page.sessions.table.ip": "Adresă IP",
"page.sessions.table.user_agent": "Agent Utilizator",
"page.sessions.title": "Sesiuni",
"page.settings.link_google_account": "Atașează contul personal Google",
"page.settings.link_oidc_account": "Atașează contul meu %s",
"page.settings.title": "Setări",
"page.settings.unlink_google_account": "Decuplează contul personal Google",
"page.settings.unlink_oidc_account": "Decuplează contul meu %s",
"page.settings.webauthn.actions": "Acțiuni",
"page.settings.webauthn.added_on": "Adăugată în",
"page.settings.webauthn.delete": [
"Elimină %d cheie de acces",
"Elimină %d chei de acces",
"Elimină %d chei de acces"
],
"page.settings.webauthn.last_seen_on": "Utilizat ultima dată",
"page.settings.webauthn.passkey_name": "Nume cheie acces",
"page.settings.webauthn.passkeys": "Chei Acces",
"page.settings.webauthn.register": "Înregistrare cheie acces",
"page.settings.webauthn.register.error": "Eroare la înregistrarea cheii de acces",
"page.shared_entries_count": [
"%d înregistrare partajată",
"%d înregistrări partajate",
"%d înregistrări partajate"
],
"page.shared_entries.title": "Înregistrări partajate",
"page.starred_entry_count": [
"%d înregistrare marcată",
"%d Înregistrări marcate",
"%d Înregistrări marcate"
],
"page.starred.title": "Marcate",
"page.total_entry_count": [
"%d intrare în total",
"%d intrări în total",
"%d intrări în total"
],
"page.unread_entry_count": [
"%d înregistrare necitită",
"%d înregistrări necitite",
"%d înregistrări necitite"
],
"page.unread.title": "Necitite",
"page.users.actions": "Acțiuni",
"page.users.admin.no": "Nu",
"page.users.admin.yes": "Da",
"page.users.is_admin": "Administrator",
"page.users.last_login": "Ultima Conectare",
"page.users.never_logged": "Niciodată",
"page.users.title": "Utilizatori",
"page.users.username": "Nume",
"page.webauthn_rename.title": "Redenumire Cheie Acces",
"pagination.first": "Prima",
"pagination.last": "Ultima",
"pagination.next": "Următor",
"pagination.previous": "Anterior",
"search.label": "Caută",
"search.placeholder": "Caută…",
"search.submit": "Caută",
"skip_to_content": "Sari la conținut",
"time_elapsed.days": [
"%d zi în urmă",
"%d zile în urmă",
"%d zile în urmă"
],
"time_elapsed.hours": [
"%d oră în urmă",
"%d ore în urmă",
"%d ore în urmă"
],
"time_elapsed.minutes": [
"%d minut în urmă",
"%d minute în urmă",
"%d minute în urmă"
],
"time_elapsed.months": [
"%d lună în urmă",
"%d luni în urmă",
"%d luni în urmă"
],
"time_elapsed.not_yet": "încă nu",
"time_elapsed.now": "chiar acum",
"time_elapsed.weeks": [
"%d săptămână în urmă",
"%d săptămâni în urmă",
"%d săptămâni în urmă"
],
"time_elapsed.years": [
"%d an în urmă",
"%d ani în urmă",
"%d ani în urmă"
],
"time_elapsed.yesterday": "ieri",
"tooltip.keyboard_shortcuts": "Scurtături Tastatură: %s",
"tooltip.logged_user": "Atentificat ca %s"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+49 -13
View File
@@ -174,7 +174,7 @@ func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithAbsoluteProxyURL(r, "localhost", input)
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
expected := `<p><img src="http://localhost/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
if expected != output {
@@ -182,12 +182,10 @@ func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
}
}
func TestAbsoluteProxyFilterWithHttpsScheme(t *testing.T) {
func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
os.Setenv("HTTPS", "1")
os.Setenv("BASE_URL", "http://example.org:88/folder/")
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -196,12 +194,25 @@ func TestAbsoluteProxyFilterWithHttpsScheme(t *testing.T) {
t.Fatalf(`Parsing failure: %v`, err)
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
if config.Opts.BaseURL() != "http://example.org:88/folder" {
t.Fatalf(`Unexpected base URL, got "%s"`, config.Opts.BaseURL())
}
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithAbsoluteProxyURL(r, "localhost", input)
expected := `<p><img src="https://localhost/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
if config.Opts.RootURL() != "http://example.org:88" {
t.Fatalf(`Unexpected root URL, got "%s"`, config.Opts.RootURL())
}
router := mux.NewRouter()
if config.Opts.BasePath() != "" {
router = router.PathPrefix(config.Opts.BasePath()).Subrouter()
}
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithAbsoluteProxyURL(router, input)
expected := `<p><img src="http://example.org:88/folder/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
@@ -225,7 +236,7 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<audio src="https://website/folder/audio.mp3"></audio>`
output := RewriteDocumentWithAbsoluteProxyURL(r, "localhost", input)
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
expected := `<audio src="http://localhost/proxy/EmBTvmU5B17wGuONkeknkptYopW_Tl6Y6_W8oYbN_Xs=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9hdWRpby5tcDM="></audio>`
if expected != output {
@@ -300,7 +311,7 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithAbsoluteProxyURL(r, "localhost", input)
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
if expected != output {
@@ -553,3 +564,28 @@ func TestProxyFilterVideoPoster(t *testing.T) {
t.Errorf(`Not expected output: got %s`, output)
}
}
func TestProxyFilterVideoPosterOnce(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image,video")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
output := RewriteDocumentWithRelativeProxyURL(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
}
}

Some files were not shown because too many files have changed in this diff Show More