Compare commits

...

148 Commits

Author SHA1 Message Date
Frédéric Guillot 4c0c658152 Update ChangeLog 2023-05-06 14:09:45 -07:00
dependabot[bot] 88062ab9f9 Bump golang.org/x/term from 0.7.0 to 0.8.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/term/compare/v0.7.0...v0.8.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>
2023-05-04 17:04:42 -07:00
Adriano Di Luzio 85856baf13 fix: Point to docs for URL rewrite rules too 2023-05-04 17:04:21 -07:00
dependabot[bot] 2d33b7df6e Bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.0 to 1.15.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.15.0...v1.15.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>
2023-05-03 17:58:32 -07:00
Davide Masserut 5d8a8878d5 Update scraping rules for ilpost.it 2023-05-02 17:07:25 -07:00
dependabot[bot] 8d2dab44d8 Bump github.com/lib/pq from 1.10.8 to 1.10.9
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.8 to 1.10.9.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.8...v1.10.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 16:19:33 -07:00
dependabot[bot] d435e67a36 Bump mvdan.cc/xurls/v2 from 2.4.0 to 2.5.0
Bumps [mvdan.cc/xurls/v2](https://github.com/mvdan/xurls) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/mvdan/xurls/releases)
- [Commits](https://github.com/mvdan/xurls/compare/v2.4.0...v2.5.0)

---
updated-dependencies:
- dependency-name: mvdan.cc/xurls/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-17 16:58:42 -07:00
Romain de Laage 33c4b5188c Add a rewrite rule to remove clickbait titles 2023-04-15 18:25:43 -07:00
dependabot[bot] 8161085714 Bump github.com/lib/pq from 1.10.7 to 1.10.8
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.7 to 1.10.8.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.7...v1.10.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-14 19:13:51 -07:00
dependabot[bot] 6493239484 Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.14.0 to 1.15.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.14.0...v1.15.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>
2023-04-13 20:48:57 -07:00
dependabot[bot] a143681af3 Bump golang.org/x/crypto from 0.7.0 to 0.8.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.7.0...v0.8.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>
2023-04-07 16:12:41 -07:00
Emiel Wiedijk 5a88e0465e Update rewrite rules for theverge.com
Articles on The Verge sometimes contain a section for related articles.
This section can be distracting in reader mode. Therefore, filter the
related article section using the scraper rules.
2023-04-07 16:12:19 -07:00
dependabot[bot] 30bb901d7c Bump golang.org/x/oauth2 from 0.6.0 to 0.7.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.6.0...v0.7.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>
2023-04-07 16:02:42 -07:00
dependabot[bot] 40418fcf6f Bump golang.org/x/net from 0.8.0 to 0.9.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.8.0...v0.9.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>
2023-04-06 17:42:43 -07:00
dependabot[bot] ad85e5be80 Bump golang.org/x/term from 0.6.0 to 0.7.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.6.0...v0.7.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>
2023-04-05 20:06:59 -07:00
Frédéric Guillot aa9b18a8d6 Make sure PROXY_IMAGES option is backward compatible
Bug introduced in PR #1610

Fixes #1753
2023-04-02 18:35:43 -07:00
Jake Walker 8b6dd3e599 Keep other table rows and columns 2023-04-02 17:50:19 -07:00
Jake Walker 49d2596fc6 Basic table removal rule 2023-04-02 17:50:19 -07:00
rook1e 9a826bbe6f feat: support searching well-known urls in subdirectory 2023-04-02 17:44:14 -07:00
rook1e acc9186a59 fix: extra-long title overflow 2023-04-02 17:37:25 -07:00
dzaikos 7d252ea45b Add swipe as option for gesture navigation between entries.
* Refactor `TouchHandler` to handle double-tap and swipe gestures.
  * Renamed existing `onTouch` JavaScript methods to `onItemTouch` and
    added `onContentTouch` methods for swipe gesture.
  * Refactor double-tap. It's now a method in `TouchHandler` versus
    anonymous functions in `listen()` method.
* Updated CSS classes.
  * Added `touch-action` CSS for `.entry-content`.
  * Renamed CSS classes for adding events in `TouchHandler`.
* Updated users settings to replace checkbox for double tap with select
  for none, double tap, or swipe.
* Added database migrations for new gesture_nav option.
  * Rename `users.double_tap` to `users.gesture_nav` and migrate
    existing user settings.
* Updated translation files. (Non-English updated with Google
  Translate.)

Resolves #1449, closes #1495
2023-03-28 18:00:57 -07:00
Frédéric Guillot 140a40acaf Use secrets.GITHUB_TOKEN to push images instead of a PAT 2023-03-27 21:29:33 -07:00
toastal 56efba66f5 Prefer typographic punctuation
For a long time, we’ve not been limited to ASCII and have machines that
can properly render the typographically-correct punctuation symbols for
our languages. This leads to a better, clearer reading experience and
also matches the `<meta charset="utf-8">` and the the use of such
punctuation on FAQs.

Changes:
• Ellipsis: `...` → `…` (https://en.wikipedia.org/wiki/Ellipsis)
• Apostrophe: `'` → `’` (https://en.wikipedia.org/wiki/Apostrophe)

While I could try to do research on other languages, I’m not a native
speaker in them and wouldn’t feel comfortable making any adjustments
outside of English.
2023-03-27 20:55:25 -07:00
Frédéric Guillot 7e612cddd3 Update issue templates 2023-03-26 19:13:53 -07:00
Davide Masserut 034e46700c Process older entries first
Feed entries are usually ordered from most to least recent.

Processing older entries first ensures that their creation timestamp
is lower than that of newer entries.

This is useful when we order by creation, because then we get a
consistent timeline.
2023-03-25 16:19:07 -07:00
Daniel Jakots ac8f64d7a1 Set Prometheus as datasource everywhere
Requested by @lnicola.
2023-03-24 20:12:13 -07:00
Daniel Jakots b536e05fee Fix grafana dashboard 2023-03-24 20:12:13 -07:00
dependabot[bot] 6eed037186 Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 20:20:59 -07:00
Frédéric Guillot 5912400dee Push Docker images to Quay.io (RedHat) 2023-03-19 21:25:05 -07:00
Frédéric Guillot ab209df78f Update ChangeLog 2023-03-16 19:34:20 -07:00
dependabot[bot] 11a352dcfd Bump github.com/tdewolff/minify/v2 from 2.12.4 to 2.12.5
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.12.4 to 2.12.5.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.12.4...v2.12.5)

---
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>
2023-03-16 18:04:23 -07:00
Frédéric Guillot 9ae6922bdc Fix null reference in toggle entry attachments shortcut
Fixes #1723
2023-03-13 20:20:35 -07:00
Frédéric Guillot ea8c3c801a Update Security policy 2023-03-13 19:56:47 -07:00
Frédéric Guillot eb9508502c Avoid XSS when opening a broken image due to unescaped ServerError in proxy handler
Creating an RSS feed item with the inline description containing an `<img>` tag
with a `srcset` attribute pointing to an invalid URL like
`http:a<script>alert(1)</script>`, we can coerce the proxy handler into an error
condition where the invalid URL is returned unescaped and in full.

This results in JavaScript execution on the Miniflux instance as soon as the
user is convinced to open the broken image.
2023-03-12 22:36:03 -07:00
Frédéric Guillot b46b5dfb2a Use r.RemoteAddr to check /metrics endpoint network access
HTTP headers like X-Forwarded-For or X-Real-Ip can be easily spoofed. As
such, it cannot be used to test if the client IP is allowed.

The recommendation is to use HTTP Basic authentication to protect the
metrics endpoint, or run Miniflux behind a trusted reverse-proxy.
2023-03-11 20:53:12 -08:00
Frédéric Guillot 877dbed5e8 Add HTTP Basic authentication for /metrics endpoint 2023-03-11 20:13:52 -08:00
fructurj 79ff381c4c Update es_ES.json 2023-03-11 17:38:07 -08:00
dependabot[bot] f6a672738a Bump golang.org/x/crypto from 0.6.0 to 0.7.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.6.0...v0.7.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>
2023-03-06 20:38:55 -08:00
dependabot[bot] e4964d6933 Bump golang.org/x/oauth2 from 0.5.0 to 0.6.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.5.0...v0.6.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>
2023-03-06 20:27:58 -08:00
Davide Masserut 755c9af47d Update scraping rules for ilpost.it 2023-03-01 20:04:25 -08:00
Frédéric Guillot 02e4b8eadc Update GitHub Actions to use Go 1.20 2023-03-01 19:56:06 -08:00
Frédéric Guillot aaa1625724 Ignore empty link when discovering feeds 2023-02-26 17:19:26 -08:00
Frédéric Guillot bb5f3ec6a8 Disable CGO explicitly to make sure the binary is statically linked
Apparently this behavior has been changed in Go 1.20: https://tip.golang.org/doc/go1.20#cgo
2023-02-25 16:55:11 -08:00
Sigsign 8804eb9a78 Update Japanese translation 2023-02-25 15:58:39 -08:00
Romain de Laage 2c2700a31d Proxy support for several media types
closes #615
closes #635
2023-02-25 15:57:59 -08:00
privatmamtora 8f9ccc6540 Parse <category> from Feeds (RSS, Atom and JSON) 2023-02-24 20:52:45 -08:00
dependabot[bot] ff8d68c151 Bump github.com/PuerkitoBio/goquery from 1.8.0 to 1.8.1
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.8.0...v1.8.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>
2023-02-20 19:24:57 -08:00
the7thNightmare 1fb0bc29db Update the plural for Indonesian
Copied from the zh_CN plural
2023-02-19 19:53:06 -08:00
Ananta Krsna dasa a1593b8942 Run the application in one command 2023-02-19 11:56:51 -08:00
Ananta Krsna dasa 20c4cb770e Bring back the health check condition to depends_on 2023-02-19 11:56:51 -08:00
Ananta Krsna dasa db7a4ae7e9 Remove deprecated version element 2023-02-19 11:56:51 -08:00
the7thNightmare aabb766fad Add Indonesian Language 2023-02-19 11:49:17 -08:00
the7thNightmare 8dce3099d9 Add Indonesian Language 2023-02-19 11:49:17 -08:00
dependabot[bot] fb2b43176f Bump golang.org/x/net from 0.6.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.6.0...v0.7.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>
2023-02-14 19:06:58 -08:00
dependabot[bot] 2f6034c63c Bump golang.org/x/crypto from 0.5.0 to 0.6.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.5.0...v0.6.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>
2023-02-09 17:54:31 -08:00
dependabot[bot] 67190fc988 Bump golang.org/x/oauth2 from 0.4.0 to 0.5.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.4.0...v0.5.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>
2023-02-09 17:49:00 -08:00
dependabot[bot] e4c0495646 Bump golang.org/x/net from 0.5.0 to 0.6.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.5.0...v0.6.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>
2023-02-08 20:15:36 -08:00
dependabot[bot] a7508b2746 Bump golang.org/x/term from 0.4.0 to 0.5.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.4.0...v0.5.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>
2023-02-07 20:16:21 -08:00
Wojtek 34408b50a7 Add CSS classes to differentiate between category/feed/entry view and icons 2023-02-06 20:46:42 -08:00
Marie Ramlow 48acd1feca Add rewrite and scraper rules for blog.cloudflare.com 2023-02-05 21:01:42 -08:00
Ryan Cao 8d51fd8ff5 fix: add color-scheme to themes 2023-02-05 20:58:23 -08:00
Martin Vietz a44ba4abcb Add toggle open/close entry attachments shortcut 2023-02-05 20:51:51 -08:00
dependabot[bot] b338c9b3c2 Bump github.com/yuin/goldmark from 1.5.3 to 1.5.4
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.5.3 to 1.5.4.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.5.3...v1.5.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>
2023-02-02 20:07:07 -08:00
xdavidwu 08f7835f5d sanitizer: allow id in <sup>
One of blogs I read uses anchor on <sup> to link a footnote back to its
reference.
2023-01-31 17:53:45 -08:00
dependabot[bot] d38fc80bad Bump docker/build-push-action from 3 to 4
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
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>
2023-01-30 17:04:35 -08:00
Frédéric Guillot b2fd84e0d3 Update ChangeLog 2023-01-29 17:01:14 -08:00
Sigsign e64f488654 Update Japanese translations 2023-01-28 17:58:56 -08:00
Sigsign 8017ed2cf6 Sort like en_US.json 2023-01-28 17:58:56 -08:00
Davide Masserut 65febebd40 Fix header items wrapping 2023-01-17 20:00:13 -08:00
Frédéric Guillot 2e047dff98 Add option to enable or disable double tap 2023-01-14 16:59:52 -08:00
Frédéric Guillot 6612e42668 Improve PWA display mode label in settings page 2023-01-14 15:39:09 -08:00
dependabot[bot] 2956bbad8d Bump golang.org/x/oauth2 from 0.3.0 to 0.4.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.3.0...v0.4.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>
2023-01-05 07:23:59 -08:00
dependabot[bot] 3285a00ebc Bump golang.org/x/crypto from 0.4.0 to 0.5.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.4.0...v0.5.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>
2023-01-05 07:23:41 -08:00
dependabot[bot] c0c8e47344 Bump golang.org/x/net from 0.4.0 to 0.5.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.4.0...v0.5.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>
2023-01-05 07:14:53 -08:00
dependabot[bot] 3fc02df70f Bump golang.org/x/term from 0.3.0 to 0.4.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.3.0...v0.4.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>
2023-01-05 07:06:56 -08:00
Tadeusz Magura-Witkowski c071201e37 Update pl_PL.json
Fixed message for form.feed.label.disable (for some reason this was in Russian?).
2022-12-29 12:56:50 -08:00
Davide Masserut 690d66ce0b Update scraping rules for ilpost.it 2022-12-27 13:33:41 -08:00
Davide Masserut ef312ef770 Update scraping rule for ilpost.it 2022-12-16 15:07:10 -08:00
Davide Masserut c0bed53b42 Add scraping rule for ilpost.it 2022-12-15 19:53:12 -08:00
Davide Masserut c0ee3ed375 Update reading time HTML element after fetching the original web page 2022-12-14 19:53:04 -08:00
Davide Masserut ce35b46fee Add category feeds refresh 2022-12-12 19:41:30 -08:00
Frédéric Guillot e12c263fc9 Update Changelog 2022-12-10 10:45:34 -08:00
dependabot[bot] 281c9be6c8 Bump golang.org/x/crypto from 0.3.0 to 0.4.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.3.0...v0.4.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>
2022-12-07 20:28:01 -08:00
dependabot[bot] b92debf9c9 Bump golang.org/x/net from 0.3.0 to 0.4.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.3.0...v0.4.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>
2022-12-07 20:16:18 -08:00
nyanpasu64 b6c368c39c Disable double-tap mobile gesture if swipe gesture is disabled
Fixes #441.
2022-12-06 20:34:15 -08:00
dependabot[bot] de92e3e472 Bump golang.org/x/oauth2 from 0.2.0 to 0.3.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.2.0...v0.3.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>
2022-12-06 19:57:18 -08:00
Harry Cheng d9777f1439 Skip integrations if there are no entries to push 2022-12-04 12:58:10 -08:00
mutantmonkey b4643fd27f Enable TLS-ALPN-01 challenge for ACME
This type of challenge works purely at the TLS layer and is compatible
with SNI proxies. The existing HTTP-01 challenge support has been left
as-is.

Fixes #1476.
2022-11-16 20:41:55 -08:00
Frédéric Guillot d9cf3f9c38 Preconfigure Miniflux for GitHub Codespaces 2022-11-17 03:49:05 +00:00
Frédéric Guillot dfd6b769b9 Create devcontainer.json 2022-11-16 17:34:55 -08:00
dependabot[bot] 68780de6fc Bump golang.org/x/crypto from 0.2.0 to 0.3.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.2.0...v0.3.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>
2022-11-16 16:18:50 -08:00
Frédéric Guillot 93715b542c Revert "scraper follow the only link"
This reverts commit 10207967c4.
2022-11-14 17:45:40 -08:00
Frédéric Guillot de1a06e3e8 Add missing check in followTheOnlyLink() that leads to a panic
Bug introduced in PR #1290. Fixes #1631.
2022-11-14 16:44:02 -08:00
Frédéric Guillot bfebf946c2 Update ChangeLog 2022-11-13 15:22:25 -08:00
Frédéric Guillot 6094130f61 Update Go dependencies 2022-11-13 14:56:43 -08:00
Frédéric Guillot d1bd0073cc Pin Postgres image version in docker-compose examples to avoid unexpected upgrades 2022-11-13 14:14:04 -08:00
Juan Pedro Bretti Mandarano 789e30fdff Locale terms normalization 2 2022-11-13 13:55:08 -08:00
Juan Pedro Bretti Mandarano 04507a8a34 Locale terms normalization #1619 2022-11-13 13:55:08 -08:00
Frédéric Guillot 1ded3f8bc7 Update integration tests 2022-11-12 21:03:43 -08:00
Frédéric Guillot 3d64162f06 Allow Content-Type and Accept headers to CORS policy 2022-11-12 21:03:43 -08:00
George Angelopoulos 79357a527f Use $(...) notation instead of legacy backticked ...
This is based on a shellcheck recommendation.
https://www.shellcheck.net/wiki/SC2006
2022-11-12 20:30:44 -08:00
George Angelopoulos 1e16e19d11 use dirs file for debian package
Instead of running mkdir from debian/rules, the native way to create the
necessary directories is with the dirs file which is read by the
dh_installdirs helper script.

See:
* https://www.debian.org/doc/manuals/maint-guide/dother.en.html#dirs
* dh_installdirs(1) manual page

I am not aware of any specific problems that could arise from doing it
manually instead of using dh_installdirs. But sticking to the native
approach might be a good idea.
2022-11-12 20:30:44 -08:00
Romain de Laage efa8bfcf0e Use custom home page in PWA 2022-11-12 20:12:39 -08:00
dependabot[bot] 1351761f81 Bump golang.org/x/term from 0.1.0 to 0.2.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.1.0 to 0.2.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.1.0...v0.2.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>
2022-11-07 20:35:16 -08:00
dependabot[bot] 962a0460cc Bump github.com/prometheus/client_golang from 1.13.0 to 1.13.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.13.0 to 1.13.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.13.1/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.13.0...v1.13.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>
2022-11-07 19:49:37 -08:00
jebbs 10207967c4 scraper follow the only link
* in some cases, what the scraper got is only a landing page, user can use scraper rules to extract the link of the landing page and follow it
* it also fix the  wrong scrape rule apply when the server redirects it to another host
2022-10-31 19:49:34 -07:00
Frédéric Guillot 242eeaf07e Update golang.org/x/* dependencies 2022-10-27 18:09:19 -07:00
Romain de Laage 550e7d0415 Add matrix bot support 2022-10-27 17:53:19 -07:00
Romain de Laage 3f14d08095 Proxify images in API responses 2022-10-27 17:33:18 -07:00
Frédéric Guillot 206be5ba15 Rename column to CategoriesSortingOrder 2022-10-25 20:06:28 -07:00
Romain de Laage 83e1f154b5 Add optional sort option in category page
closes #1552
2022-10-25 20:06:28 -07:00
Romain de Laage ec47106c26 Remove dependency to go-server-timing
fix #1586
2022-10-23 16:59:05 -07:00
Dave Marquard 74b69a4c7c Add support for the continuation parameter and result for Google Reader API ID calls
Support the ``continuation parameter and result for Google Reader API ID calls. Allows clients to support paging of results / fetching additional results when the number of entries exceed the limit parameter passed in by the client.
2022-10-23 16:49:45 -07:00
George Angelopoulos 84d5a9a354 Use automatic variable for build target file names 2022-10-19 20:37:28 -07:00
Romain de Laage eb86773039 Recalbox rewrite rule 2022-10-19 20:13:44 -07:00
Michael van Tricht 44ca5b8591 Fix reading time dutch text 2022-10-17 21:16:41 -07:00
Frédéric Guillot 95e125dfc7 Update ChangeLog 2022-10-16 14:29:17 -07:00
jgbresson 7f6ce16d85 Add scraping rules for theverge.com 2022-10-16 11:58:35 -07:00
jgbresson aa47789f55 Add add_dynamic_image rewrite rule for theverge.com 2022-10-16 11:57:01 -07:00
Dave Marquard 7a961babd4 support date filtering for all google reader api item ID calls 2022-10-16 11:43:34 -07:00
Frédéric Guillot d947b0194b Handle RSS entries with only a GUID permalink 2022-10-09 16:58:25 -07:00
jtagcat cd7f01f573 client.New(): accept endpoint URLs with /v1/ 2022-10-03 19:49:45 -07:00
dependabot[bot] 159abf7b7d Bump github.com/tdewolff/minify/v2 from 2.12.2 to 2.12.4
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.12.2 to 2.12.4.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.12.2...v2.12.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>
2022-10-03 17:41:04 -07:00
dependabot[bot] 1b6eafa7d9 Bump github.com/yuin/goldmark from 1.4.15 to 1.5.2
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.4.15 to 1.5.2.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.4.15...v1.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-26 16:12:44 -07:00
dependabot[bot] 42928e0a9d Bump github.com/tdewolff/minify/v2 from 2.12.1 to 2.12.2
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.12.1 to 2.12.2.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.12.1...v2.12.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>
2022-09-26 16:12:16 -07:00
Tomas Peterka 70c58bd346 API: Allow Basic authorization header 2022-09-19 20:56:27 -07:00
dependabot[bot] 556d88c96d Bump github.com/yuin/goldmark from 1.4.14 to 1.4.15
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.4.14 to 1.4.15.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.4.14...v1.4.15)

---
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>
2022-09-19 19:29:05 -07:00
Frédéric Guillot e0aa9459c9 Remove Scorecards GitHub Action 2022-09-18 17:40:17 -07:00
Frédéric Guillot e8fe4ed881 Log feed URL when submitting a subscription that returns an error 2022-09-18 17:37:53 -07:00
Frédéric Guillot 98c166c950 Update "make run" command to execute migrations 2022-09-18 16:57:40 -07:00
jtagcat 3f64e4b943 Add option to send only the URL to Wallabag 2022-09-18 16:52:28 -07:00
ericgaspar 133a1f83e3 Fix typo 2022-09-18 10:49:49 -07:00
dependabot[bot] c50a7d9938 Bump ossf/scorecard-action from 1.1.2 to 2.0.3
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 1.1.2 to 2.0.3.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/ce330fde6b1a5c9c75b417e7efc510b822a35564...865b4092859256271290c77adbd10a43f4779972)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 19:21:35 -07:00
dependabot[bot] 753940e0cf Bump github.com/lib/pq from 1.10.6 to 1.10.7
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.6 to 1.10.7.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.6...v1.10.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 19:21:11 -07:00
Frédéric Guillot 138fd926ee Do not convert anchors to absolute links 2022-09-11 22:40:52 -07:00
dependabot[bot] 183cb491b3 Bump github.com/yuin/goldmark from 1.4.13 to 1.4.14
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.4.13 to 1.4.14.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.4.13...v1.4.14)

---
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>
2022-09-05 15:36:33 -07:00
dependabot[bot] ee070de860 Bump github.com/tdewolff/minify/v2 from 2.12.0 to 2.12.1
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.12.0...v2.12.1)

---
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>
2022-09-05 15:34:58 -07:00
Nicole bbc087d2ad Allow setting a custom image proxy URL 2022-08-29 20:33:47 -07:00
Frédéric Guillot b8c3153b75 Update dependencies 2022-08-28 20:50:45 -07:00
Romain de Laage d7f978da49 Allow zoom on mobile devices
See https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#user-scalable
2022-08-21 21:33:32 -07:00
Frédéric Guillot 0afad251e0 Fix integration tests on GitHub Actions 2022-08-21 15:31:52 -07:00
Adam B 4d847c6a74 Add scraping rule for royalroad.com
This is what I use for several stories I follow, and I thought it might be useful to other miniflux users.
2022-08-17 19:25:39 -07:00
Owen Valentine f404ddde91 Add swordscomic.com 2022-08-17 19:23:29 -07:00
Owen Valentine c8a3d953cf Add smbc-comics.com 2022-08-17 19:23:29 -07:00
Owen Valentine f851ecac78 Sort alphabetically 2022-08-17 19:23:29 -07:00
Leonid Shevtsov 1c1fcdcd3d Tweak page.categories.feed_count for UA locale 2022-08-15 20:20:23 -07:00
Leonid Shevtsov 594e9a6039 Add Ukrainian locale 2022-08-15 20:20:23 -07:00
Frédéric Guillot ffafa810da Update golang.org/x/crypto 2022-08-13 21:58:45 -07:00
141 changed files with 4581 additions and 1845 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Miniflux",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"remoteUser": "vscode",
"forwardPorts": [
8080
],
"customizations": {
"vscode": {
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
"extensions": [
"ms-azuretools.vscode-docker",
"golang.go"
]
}
}
}
+27
View File
@@ -0,0 +1,27 @@
version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/go
volumes:
- ..:/workspace:cached
command: sleep infinity
network_mode: service:db
environment:
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
db:
image: postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
hostname: postgres
environment:
POSTGRES_DB: miniflux2
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
volumes:
postgres-data: null
+1 -1
View File
@@ -2,7 +2,7 @@
name: Bug report
about: Create a bug report
title: ''
labels: bug
labels: bug, question / discussion
assignees: ''
---
@@ -1,10 +0,0 @@
---
name: Installation Issue
about: Do you need help to install Miniflux?
title: ''
labels: installation issue
assignees: ''
---
@@ -1,10 +0,0 @@
---
name: Question / Discussion
about: Open discussions
title: ''
labels: question / discussion
assignees: ''
---
+1 -1
View File
@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
reviewers:
- "fguillot"
assignees:
+5 -5
View File
@@ -14,10 +14,10 @@ jobs:
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
go-version: [1.18, 1.19]
go-version: ["1.19", "1.20"]
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
@@ -40,13 +40,13 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: 1.19
go-version: "1.20"
- name: Checkout
uses: actions/checkout@v3
- name: Install Postgres client
run: sudo apt-get install -y postgresql-client
run: sudo apt update && sudo apt install -y postgresql-client
- name: Run integration tests
run: make integration-test
env:
+16 -8
View File
@@ -1,5 +1,4 @@
name: Docker
permissions: read-all
on:
schedule:
- cron: '0 1 * * *'
@@ -8,6 +7,8 @@ on:
- '*.*.*'
jobs:
docker-images:
permissions:
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -22,10 +23,10 @@ jobs:
DOCKER_VERSION=dev
if [ "${{ github.event_name }}" = "schedule" ]; then
DOCKER_VERSION=nightly
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
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_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest"
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}
@@ -36,10 +37,10 @@ jobs:
DOCKER_VERSION=dev-distroless
if [ "${{ github.event_name }}" = "schedule" ]; then
DOCKER_VERSION=nightly-distroless
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
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_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},${DOCKER_IMAGE}:latest-distroless,ghcr.io/${DOCKER_IMAGE}:latest-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}
@@ -60,10 +61,17 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Quay Container Registry
uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}
- name: Build and Push Alpine images
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./packaging/docker/alpine/Dockerfile
@@ -72,7 +80,7 @@ jobs:
tags: ${{ steps.docker_alpine_tag.outputs.tags }}
- name: Build and Push Distroless images
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./packaging/docker/distroless/Dockerfile
+2 -3
View File
@@ -23,10 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: 1.19
go-version: "1.20"
- uses: golangci/golangci-lint-action@v3
with:
args: --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
skip-go-installation: true
-55
View File
@@ -1,55 +0,0 @@
name: Scorecards supply-chain security
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '31 8 * * 6'
push:
branches: [ main ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
actions: read
contents: read
steps:
- name: "Checkout code"
uses: actions/checkout@v3 # v2.4.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@ce330fde6b1a5c9c75b417e7efc510b822a35564 # v1.0.1
with:
results_file: results.sarif
results_format: sarif
# Read-only PAT token. To create it,
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
# Publish the results to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`,
# regardless of the value entered here.
publish_results: true
# Upload the results as artifacts (optional).
- name: "Upload artifact"
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v2.3.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
with:
sarif_file: results.sarif
+133
View File
@@ -1,3 +1,136 @@
Version 2.0.44 (May 6, 2023)
----------------------------
* Add link to the URL rewrite rules documentation
* Update scraping rules for `ilpost.it`
* Update rewrite rules for `theverge.com`
* Add a rewrite rule to remove clickbait titles
* Make sure `PROXY_IMAGES` option is backward compatible with `PROXY_OPTION` and `PROXY_MEDIA_TYPES`
* Add new rule to remove tables
* Add support for searching well-known URLs in subdirectory
* Add CSS `word-wrap` rule to break very long entry title into multiple lines
* Add swipe as option for gesture navigation between entries. There are now 3 possible choices: `none`, `double-tap`, and `swipe`.
* Prefer typographic punctuation in English translation
* Process older entries first:
- Feed entries are usually ordered from most to least recent.
- Processing older entries first ensures that their creation timestamp
is lower than that of newer entries.
- This is useful when we order by creation, because then we get a
consistent timeline.
* Fix Grafana dashboard
* Push Docker images to `Quay.io` (RedHat)
* Bump `golang.org/x/*`, `github.com/lib/pq`, `mvdan.cc/xurls/v2` and `github.com/prometheus/client_golang` dependencies
Version 2.0.43 (March 16, 2023)
-------------------------------
* Avoid XSS when opening a broken image due to unescaped ServerError in proxy handler (CVE-2023-27592)
Creating an RSS feed item with the inline description containing an `<img>` tag
with a `srcset` attribute pointing to an invalid URL like
`http:a<script>alert(1)</script>`, we can coerce the proxy handler into an error
condition where the invalid URL is returned unescaped and in full.
This results in JavaScript execution on the Miniflux instance as soon as the
user is convinced to open the broken image.
* Use `r.RemoteAddr` to check `/metrics` endpoint network access (CVE-2023-27591)
HTTP headers like `X-Forwarded-For` or `X-Real-Ip` can be easily spoofed. As
such, it cannot be used to test if the client IP is allowed.
The recommendation is to use HTTP Basic authentication to protect the
metrics endpoint, or run Miniflux behind a trusted reverse-proxy.
* Add HTTP Basic authentication for `/metrics` endpoint
* Add proxy support for several media types
* Parse feed categories from RSS, Atom and JSON feeds
* Ignore empty link when discovering feeds
* Disable CGO explicitly to make sure the binary is statically linked
* Add CSS classes to differentiate between category/feed/entry view and icons
* Add rewrite and scraper rules for `blog.cloudflare.com`
* Add `color-scheme` to themes
* Add new keyboard shortcut to toggle open/close entry attachments section
* Sanitizer: allow `id` attribute in `<sup>` element
* Add Indonesian Language
* Update translations
* Update Docker Compose examples:
- Run the application in one command
- Bring back the health check condition to `depends_on`
- Remove deprecated `version` element
* Update scraping rules for `ilpost.it`
* Bump `github.com/PuerkitoBio/goquery` from `1.8.0` to `1.8.1`
* Bump `github.com/tdewolff/minify/v2` from `2.12.4` to `2.12.5`
* Bump `github.com/yuin/goldmark` from `1.5.3` to `1.5.4`
* Bump `golang.org/x/*` dependencies
Version 2.0.42 (January 29, 2023)
---------------------------------
* Fix header items wrapping
* Add option to enable or disable double tap
* Improve PWA display mode label in settings page
* Bump `golang.org/x/*` dependencies
* Update translations
* Add scraping rule for `ilpost.it`
* Update reading time HTML element after fetching the original web page
* Add category feeds refresh feature
Version 2.0.41 (December 10, 2022)
----------------------------------
* Reverted PR #1290 (follow the only link) because it leads to several panics/segfaults that prevent feed updates
* Disable double-tap mobile gesture if swipe gesture is disabled
* Skip integrations if there are no entries to push
* Enable TLS-ALPN-01 challenge for ACME
- This type of challenge works purely at the TLS layer and is compatible
with SNI proxies. The existing HTTP-01 challenge support has been left
as-is.
* Preconfigure Miniflux for GitHub Codespaces
* Updated `golang.org/x/net/*` dependencies
Version 2.0.40 (November 13, 2022)
----------------------------------
* Update dependencies
* Pin Postgres image version in Docker Compose examples to avoid unexpected upgrades
* Make English and Spanish translation more consistent:
- Use "Feed" everywhere instead of "Subscription"
- Use "Entry" instead of "Article"
* Allow Content-Type and Accept headers in CORS policy
* Use dirs file for Debian package
* Use custom home page in PWA manifest
* Fix scraper rule that could be incorrect when there is a redirect
* Improve web scraper to fetch the only link present as workaround to some landing pages
* Add Matrix bot integration
* Proxify images in API responses
* Add new options in user preferences to configure sorting of entries in the category page
* Remove dependency on `github.com/mitchellh/go-server-timing`
* Add support for the `continuation` parameter and result for Google Reader API ID calls
* Use automatic variable for build target file names
* Add rewrite rule for `recalbox.com`
* Improve Dutch translation
Version 2.0.39 (October 16, 2022)
---------------------------------
* Add support for date filtering in Google Reader API item ID calls
* Handle RSS entries with only a GUID permalink
* Go API Client: Accept endpoint URLs ending with `/v1/`
* CORS API headers: Allow `Basic` authorization header
* Log feed URL when submitting a subscription that returns an error
* Update `make run` command to execute migrations automatically
* Add option to send only the URL to Wallabag
* Do not convert anchors to absolute links
* Add config option to use a custom image proxy URL
* Allow zoom on mobile devices
* Add scraping rules for `theverge.com`, `royalroad.com`, `swordscomic.com`, and `smbc-comics.com`
* Add Ukrainian translation
* Update `golang.org/x/*` dependencies
* Bump `github.com/tdewolff/minify/v2` from `2.12.0` to `2.12.4`
* Bump `github.com/yuin/goldmark` from `1.4.13` to `1.5.2`
* Bump `github.com/lib/pq` from `1.10.6` to `1.10.7`
Version 2.0.38 (August 13, 2022)
--------------------------------
+17 -17
View File
@@ -46,58 +46,58 @@ miniflux:
@ go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
linux-amd64:
@ GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-arm64:
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-arm64 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv7:
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv6:
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv6 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv5:
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv5 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
darwin-amd64:
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-amd64 main.go
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
darwin-arm64:
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-arm64 main.go
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
freebsd-amd64:
@ GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-amd64 main.go
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
openbsd-amd64:
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-openbsd-amd64 main.go
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
windows-amd64:
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-amd64 main.go
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
# NOTE: unsupported targets
netbsd-amd64:
@ GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-amd64 main.go
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-x86:
@ GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-x86 main.go
@ CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
freebsd-x86:
@ GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
netbsd-x86:
@ GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-x86 main.go
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
openbsd-x86:
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
windows-x86:
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
run:
@ LOG_DATE_TIME=1 go run main.go -debug
@ LOG_DATE_TIME=1 DEBUG=1 RUN_MIGRATIONS=1 go run main.go
clean:
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
+3 -1
View File
@@ -6,4 +6,6 @@ Only the latest stable version is supported.
## Reporting a Vulnerability
Send an email to `security AT miniflux DOT net` with all the steps to reproduce the problem.
Preferably, [report the vulnerability privately using GitHub](https://github.com/miniflux/v2/security/advisories/new) ([documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)).
If you do not want to use GitHub, send an email to `security AT miniflux DOT net` with all the steps to reproduce the problem.
+5 -3
View File
@@ -14,13 +14,14 @@ import (
)
type handler struct {
store *storage.Storage
pool *worker.Pool
store *storage.Storage
pool *worker.Pool
router *mux.Router
}
// Serve declares API routes for the application.
func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
handler := &handler{store, pool}
handler := &handler{store, pool, router}
sr := router.PathPrefix("/v1").Subrouter()
middleware := newMiddleware(store)
@@ -42,6 +43,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete)
sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}/feeds", handler.getCategoryFeeds).Methods(http.MethodGet)
sr.HandleFunc("/categories/{categoryID}/refresh", handler.refreshCategory).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}/entries", handler.getCategoryEntries).Methods(http.MethodGet)
sr.HandleFunc("/categories/{categoryID}/entries/{entryID}", handler.getCategoryEntry).Methods(http.MethodGet)
sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
+17
View File
@@ -123,3 +123,20 @@ func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
json.NoContent(w, r)
}
func (h *handler) refreshCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")
jobs, err := h.store.NewCategoryBatch(userID, categoryID, h.store.CountFeeds(userID))
if err != nil {
json.ServerError(w, r, err)
return
}
go func() {
h.pool.Push(jobs)
}()
json.NoContent(w, r)
}
+29 -4
View File
@@ -9,17 +9,21 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"miniflux.app/config"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/proxy"
"miniflux.app/reader/processor"
"miniflux.app/storage"
"miniflux.app/url"
"miniflux.app/validator"
)
func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) {
func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) {
entry, err := b.GetEntry()
if err != nil {
json.ServerError(w, r, err)
@@ -31,6 +35,20 @@ func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.Entr
return
}
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
proxyOption := config.Opts.ProxyOption()
for i := range entry.Enclosures {
if proxyOption == "all" || proxyOption != "none" && !url.IsHTTPS(entry.Enclosures[i].URL) {
for _, mediaType := range config.Opts.ProxyMediaTypes() {
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
break
}
}
}
}
json.OK(w, r, entry)
}
@@ -42,7 +60,7 @@ func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
builder.WithFeedID(feedID)
builder.WithEntryID(entryID)
getEntryFromBuilder(w, r, builder)
h.getEntryFromBuilder(w, r, builder)
}
func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
@@ -53,7 +71,7 @@ func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
builder.WithCategoryID(categoryID)
builder.WithEntryID(entryID)
getEntryFromBuilder(w, r, builder)
h.getEntryFromBuilder(w, r, builder)
}
func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
@@ -61,7 +79,7 @@ func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
builder.WithEntryID(entryID)
getEntryFromBuilder(w, r, builder)
h.getEntryFromBuilder(w, r, builder)
}
func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
@@ -119,6 +137,8 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
return
}
tags := request.QueryStringParamList(r, "tags")
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithFeedID(feedID)
builder.WithCategoryID(categoryID)
@@ -127,6 +147,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
builder.WithDirection(direction)
builder.WithOffset(offset)
builder.WithLimit(limit)
builder.WithTags(tags)
configureFilters(builder, r)
entries, err := builder.GetEntries()
@@ -141,6 +162,10 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
return
}
for i := range entries {
entries[i].Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entries[i].Content)
}
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
}
+1 -1
View File
@@ -25,7 +25,7 @@ func (m *middleware) handleCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token")
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, Authorization, Content-Type, Accept")
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Max-Age", "3600")
w.WriteHeader(http.StatusOK)
+5 -5
View File
@@ -10,13 +10,13 @@ import (
"os"
"strings"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)
func askCredentials() (string, string) {
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) {
if !term.IsTerminal(fd) {
fmt.Fprintf(os.Stderr, "This is not a terminal, exiting.\n")
os.Exit(1)
}
@@ -28,9 +28,9 @@ func askCredentials() (string, string) {
fmt.Print("Enter Password: ")
state, _ := terminal.GetState(fd)
defer terminal.Restore(fd, state)
bytePassword, _ := terminal.ReadPassword(fd)
state, _ := term.GetState(fd)
defer term.Restore(fd, state)
bytePassword, _ := term.ReadPassword(fd)
fmt.Printf("\n")
return strings.TrimSpace(username), strings.TrimSpace(string(bytePassword))
+12 -2
View File
@@ -10,6 +10,7 @@ import (
"io"
"net/url"
"strconv"
"strings"
)
// Client holds API procedure calls.
@@ -19,6 +20,11 @@ type Client struct {
// New returns a new Miniflux client.
func New(endpoint string, credentials ...string) *Client {
// Web gives "API Endpoint = https://miniflux.app/v1/", it doesn't work (/v1/v1/me)
endpoint = strings.TrimSuffix(endpoint, "/")
endpoint = strings.TrimSuffix(endpoint, "/v1")
// trim to https://miniflux.app
if len(credentials) == 2 {
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
}
@@ -181,7 +187,6 @@ func (c *Client) CreateCategory(title string) (*Category, error) {
body, err := c.request.Post("/v1/categories", map[string]interface{}{
"title": title,
})
if err != nil {
return nil, err
}
@@ -201,7 +206,6 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
"title": title,
})
if err != nil {
return nil, err
}
@@ -244,6 +248,12 @@ func (c *Client) DeleteCategory(categoryID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
}
// RefreshCategory refreshes a category.
func (c *Client) RefreshCategory(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/refresh", categoryID), nil)
return err
}
// Feeds gets all feeds.
func (c *Client) Feeds() (Feeds, error) {
body, err := c.request.Get("/v1/feeds")
+45 -40
View File
@@ -18,27 +18,29 @@ const (
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"`
CJKReadingSpeed int `json:"cjk_reading_speed"`
DefaultHomePage string `json:"default_home_page"`
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
GestureNav string `json:"gesture_nav"`
LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"`
CJKReadingSpeed int `json:"cjk_reading_speed"`
DefaultHomePage string `json:"default_home_page"`
CategoriesSortingOrder string `json:"categories_sorting_order"`
}
func (u User) String() string {
@@ -56,25 +58,27 @@ type UserCreationRequest struct {
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
IsAdmin *bool `json:"is_admin"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"`
DefaultHomePage *string `json:"default_home_page"`
Username *string `json:"username"`
Password *string `json:"password"`
IsAdmin *bool `json:"is_admin"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
GestureNav *string `json:"gesture_nav"`
DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"`
DefaultHomePage *string `json:"default_home_page"`
CategoriesSortingOrder *string `json:"categories_sorting_order"`
}
// Users represents a list of users.
@@ -214,6 +218,7 @@ type Entry struct {
ReadingTime int `json:"reading_time"`
Enclosures Enclosures `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
Tags []string `json:"tags"`
}
// Entries represents a list of entries.
+194 -9
View File
@@ -1163,7 +1163,97 @@ func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
}
}
func TestProxyImages(t *testing.T) {
func TestProxyOption(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_OPTION", "all")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "all"
result := opts.ProxyOption()
if result != expected {
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
}
}
func TestDefaultProxyOptionValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultProxyOption
result := opts.ProxyOption()
if result != expected {
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
}
}
func TestProxyMediaTypes(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{"audio", "image"}
if len(expected) != len(opts.ProxyMediaTypes()) {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
resultMap := make(map[string]bool)
for _, mediaType := range opts.ProxyMediaTypes() {
resultMap[mediaType] = true
}
for _, mediaType := range expected {
if !resultMap[mediaType] {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
}
}
func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_MEDIA_TYPES", "image,audio, image")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{"audio", "image"}
if len(expected) != len(opts.ProxyMediaTypes()) {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
resultMap := make(map[string]bool)
for _, mediaType := range opts.ProxyMediaTypes() {
resultMap[mediaType] = true
}
for _, mediaType := range expected {
if !resultMap[mediaType] {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
}
}
func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
@@ -1173,15 +1263,30 @@ func TestProxyImages(t *testing.T) {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "all"
result := opts.ProxyImages()
expected := []string{"image"}
if len(expected) != len(opts.ProxyMediaTypes()) {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
if result != expected {
t.Fatalf(`Unexpected PROXY_IMAGES value, got %q instead of %q`, result, expected)
resultMap := make(map[string]bool)
for _, mediaType := range opts.ProxyMediaTypes() {
resultMap[mediaType] = true
}
for _, mediaType := range expected {
if !resultMap[mediaType] {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
}
expectedProxyOption := "all"
result := opts.ProxyOption()
if result != expectedProxyOption {
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
}
}
func TestDefaultProxyImagesValue(t *testing.T) {
func TestDefaultProxyMediaTypes(t *testing.T) {
os.Clearenv()
parser := NewParser()
@@ -1190,11 +1295,56 @@ func TestDefaultProxyImagesValue(t *testing.T) {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultProxyImages
result := opts.ProxyImages()
expected := []string{"image"}
if len(expected) != len(opts.ProxyMediaTypes()) {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
resultMap := make(map[string]bool)
for _, mediaType := range opts.ProxyMediaTypes() {
resultMap[mediaType] = true
}
for _, mediaType := range expected {
if !resultMap[mediaType] {
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
}
}
}
func TestProxyHTTPClientTimeout(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_HTTP_CLIENT_TIMEOUT", "24")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := 24
result := opts.ProxyHTTPClientTimeout()
if result != expected {
t.Fatalf(`Unexpected PROXY_IMAGES value, got %q instead of %q`, result, expected)
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
}
}
func TestDefaultProxyHTTPClientTimeoutValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultProxyHTTPClientTimeout
result := opts.ProxyHTTPClientTimeout()
if result != expected {
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
}
}
@@ -1297,6 +1447,41 @@ func TestDefaultHTTPClientMaxBodySizeValue(t *testing.T) {
}
}
func TestHTTPServerTimeout(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_SERVER_TIMEOUT", "342")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := 342
result := opts.HTTPServerTimeout()
if result != expected {
t.Fatalf(`Unexpected HTTP_SERVER_TIMEOUT value, got %d instead of %d`, result, expected)
}
}
func TestDefaultHTTPServerTimeoutValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultHTTPServerTimeout
result := opts.HTTPServerTimeout()
if result != expected {
t.Fatalf(`Unexpected HTTP_SERVER_TIMEOUT value, got %d instead of %d`, result, expected)
}
}
func TestParseConfigFile(t *testing.T) {
content := []byte(`
# This is a comment
+72 -8
View File
@@ -5,6 +5,7 @@
package config // import "miniflux.app/config"
import (
"crypto/rand"
"fmt"
"sort"
"strings"
@@ -45,7 +46,10 @@ const (
defaultCleanupArchiveUnreadDays = 180
defaultCleanupArchiveBatchSize = 10000
defaultCleanupRemoveSessionsDays = 30
defaultProxyImages = "http-only"
defaultProxyHTTPClientTimeout = 120
defaultProxyOption = "http-only"
defaultProxyMediaTypes = "image"
defaultProxyUrl = ""
defaultFetchYouTubeWatchTime = false
defaultCreateAdmin = false
defaultAdminUsername = ""
@@ -60,6 +64,7 @@ const (
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15
defaultHTTPClientProxy = ""
defaultHTTPServerTimeout = 300
defaultAuthProxyHeader = ""
defaultAuthProxyUserCreation = false
defaultMaintenanceMode = false
@@ -67,6 +72,8 @@ const (
defaultMetricsCollector = false
defaultMetricsRefreshInterval = 60
defaultMetricsAllowedNetworks = "127.0.0.1/8"
defaultMetricsUsername = ""
defaultMetricsPassword = ""
defaultWatchdog = true
defaultInvidiousInstance = "yewtu.be"
)
@@ -115,7 +122,10 @@ type Options struct {
createAdmin bool
adminUsername string
adminPassword string
proxyImages string
proxyHTTPClientTimeout int
proxyOption string
proxyMediaTypes []string
proxyUrl string
fetchYouTubeWatchTime bool
oauth2UserCreationAllowed bool
oauth2ClientID string
@@ -128,6 +138,7 @@ type Options struct {
httpClientMaxBodySize int64
httpClientProxy string
httpClientUserAgent string
httpServerTimeout int
authProxyHeader string
authProxyUserCreation bool
maintenanceMode bool
@@ -135,12 +146,18 @@ type Options struct {
metricsCollector bool
metricsRefreshInterval int
metricsAllowedNetworks []string
metricsUsername string
metricsPassword string
watchdog bool
invidiousInstance string
proxyPrivateKey []byte
}
// NewOptions returns Options with default values.
func NewOptions() *Options {
randomKey := make([]byte, 16)
rand.Read(randomKey)
return &Options{
HTTPS: defaultHTTPS,
logDateTime: defaultLogDateTime,
@@ -174,7 +191,10 @@ func NewOptions() *Options {
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
workerPoolSize: defaultWorkerPoolSize,
createAdmin: defaultCreateAdmin,
proxyImages: defaultProxyImages,
proxyHTTPClientTimeout: defaultProxyHTTPClientTimeout,
proxyOption: defaultProxyOption,
proxyMediaTypes: []string{defaultProxyMediaTypes},
proxyUrl: defaultProxyUrl,
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
oauth2ClientID: defaultOAuth2ClientID,
@@ -187,6 +207,7 @@ func NewOptions() *Options {
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
httpClientProxy: defaultHTTPClientProxy,
httpClientUserAgent: defaultHTTPClientUserAgent,
httpServerTimeout: defaultHTTPServerTimeout,
authProxyHeader: defaultAuthProxyHeader,
authProxyUserCreation: defaultAuthProxyUserCreation,
maintenanceMode: defaultMaintenanceMode,
@@ -194,8 +215,11 @@ func NewOptions() *Options {
metricsCollector: defaultMetricsCollector,
metricsRefreshInterval: defaultMetricsRefreshInterval,
metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks},
metricsUsername: defaultMetricsUsername,
metricsPassword: defaultMetricsPassword,
watchdog: defaultWatchdog,
invidiousInstance: defaultInvidiousInstance,
proxyPrivateKey: randomKey,
}
}
@@ -405,9 +429,24 @@ func (o *Options) FetchYouTubeWatchTime() bool {
return o.fetchYouTubeWatchTime
}
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyImages() string {
return o.proxyImages
// ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyOption() string {
return o.proxyOption
}
// ProxyMediaTypes returns a slice of media types to proxy.
func (o *Options) ProxyMediaTypes() []string {
return o.proxyMediaTypes
}
// ProxyUrl returns a string of a URL to use to proxy image requests
func (o *Options) ProxyUrl() string {
return o.proxyUrl
}
// ProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
func (o *Options) ProxyHTTPClientTimeout() int {
return o.proxyHTTPClientTimeout
}
// HasHTTPService returns true if the HTTP service is enabled.
@@ -443,6 +482,11 @@ func (o *Options) HTTPClientProxy() string {
return o.httpClientProxy
}
// HTTPServerTimeout returns the time limit in seconds before the HTTP server cancel the request.
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 != ""
@@ -475,6 +519,14 @@ func (o *Options) MetricsAllowedNetworks() []string {
return o.metricsAllowedNetworks
}
func (o *Options) MetricsUsername() string {
return o.metricsUsername
}
func (o *Options) MetricsPassword() string {
return o.metricsPassword
}
// HTTPClientUserAgent returns the global User-Agent header for miniflux.
func (o *Options) HTTPClientUserAgent() string {
return o.httpClientUserAgent
@@ -490,6 +542,11 @@ func (o *Options) InvidiousInstance() string {
return o.invidiousInstance
}
// ProxyPrivateKey returns the private key used by the media proxy
func (o *Options) ProxyPrivateKey() []byte {
return o.proxyPrivateKey
}
// SortedOptions returns options as a list of key value pairs, sorted by keys.
func (o *Options) SortedOptions(redactSecret bool) []*Option {
var keyValues = map[string]interface{}{
@@ -522,6 +579,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"HTTP_CLIENT_PROXY": o.httpClientProxy,
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
"HTTP_SERVICE": o.httpService,
"KEY_FILE": o.certKeyFile,
"INVIDIOUS_INSTANCE": o.invidiousInstance,
@@ -532,6 +590,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","),
"METRICS_COLLECTOR": o.metricsCollector,
"METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval,
"METRICS_USERNAME": o.metricsUsername,
"METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret),
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint,
@@ -542,7 +602,11 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"POLLING_FREQUENCY": o.pollingFrequency,
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
"POLLING_SCHEDULER": o.pollingScheduler,
"PROXY_IMAGES": o.proxyImages,
"PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout,
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
"PROXY_MEDIA_TYPES": o.proxyMediaTypes,
"PROXY_OPTION": o.proxyOption,
"PROXY_URL": o.proxyUrl,
"ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations,
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
@@ -578,7 +642,7 @@ func (o *Options) String() string {
func redactSecretValue(value string, redactSecret bool) string {
if redactSecret && value != "" {
return "******"
return "<secret>"
}
return value
}
+44 -2
View File
@@ -7,6 +7,7 @@ package config // import "miniflux.app/config"
import (
"bufio"
"bytes"
"crypto/rand"
"errors"
"fmt"
"io"
@@ -137,8 +138,20 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
// kept for compatibility purpose
case "PROXY_IMAGES":
p.opts.proxyImages = parseString(value, defaultProxyImages)
p.opts.proxyOption = parseString(value, defaultProxyOption)
case "PROXY_HTTP_CLIENT_TIMEOUT":
p.opts.proxyHTTPClientTimeout = parseInt(value, defaultProxyHTTPClientTimeout)
case "PROXY_OPTION":
p.opts.proxyOption = parseString(value, defaultProxyOption)
case "PROXY_MEDIA_TYPES":
p.opts.proxyMediaTypes = parseStringList(value, []string{defaultProxyMediaTypes})
// kept for compatibility purpose
case "PROXY_IMAGE_URL":
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
case "PROXY_URL":
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
case "CREATE_ADMIN":
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
case "ADMIN_USERNAME":
@@ -177,6 +190,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "HTTP_SERVER_TIMEOUT":
p.opts.httpServerTimeout = parseInt(value, defaultHTTPServerTimeout)
case "AUTH_PROXY_HEADER":
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
case "AUTH_PROXY_USER_CREATION":
@@ -191,12 +206,24 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval)
case "METRICS_ALLOWED_NETWORKS":
p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
case "METRICS_USERNAME":
p.opts.metricsUsername = parseString(value, defaultMetricsUsername)
case "METRICS_USERNAME_FILE":
p.opts.metricsUsername = readSecretFile(value, defaultMetricsUsername)
case "METRICS_PASSWORD":
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
case "METRICS_PASSWORD_FILE":
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
case "FETCH_YOUTUBE_WATCH_TIME":
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
case "WATCHDOG":
p.opts.watchdog = parseBool(value, defaultWatchdog)
case "INVIDIOUS_INSTANCE":
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
case "PROXY_PRIVATE_KEY":
randomKey := make([]byte, 16)
rand.Read(randomKey)
p.opts.proxyPrivateKey = parseBytes(value, randomKey)
}
}
@@ -269,14 +296,29 @@ func parseStringList(value string, fallback []string) []string {
}
var strList []string
strMap := make(map[string]bool)
items := strings.Split(value, ",")
for _, item := range items {
strList = append(strList, strings.TrimSpace(item))
itemValue := strings.TrimSpace(item)
if _, found := strMap[itemValue]; !found {
strMap[itemValue] = true
strList = append(strList, itemValue)
}
}
return strList
}
func parseBytes(value string, fallback []byte) []byte {
if value == "" {
return fallback
}
return []byte(value)
}
func readSecretFile(filename, fallback string) string {
data, err := os.ReadFile(filename)
if err != nil {
+1 -2
View File
@@ -8,6 +8,5 @@ Here are few Docker Compose examples:
- `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
```bash
docker-compose -f basic.yml up -d db
docker-compose -f basic.yml up
docker compose -f basic.yml up -d
```
+3 -3
View File
@@ -1,4 +1,3 @@
version: '3.4'
services:
miniflux:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
@@ -7,7 +6,8 @@ services:
ports:
- "80:8080"
depends_on:
- db
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
@@ -19,7 +19,7 @@ services:
# healthcheck:
# test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
db:
image: postgres:latest
image: postgres:15
container_name: postgres
environment:
- POSTGRES_USER=miniflux
+3 -3
View File
@@ -1,4 +1,3 @@
version: '3.4'
services:
caddy:
image: caddy:2
@@ -16,7 +15,8 @@ services:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
depends_on:
- db
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
@@ -25,7 +25,7 @@ services:
- ADMIN_PASSWORD=test123
- BASE_URL=https://miniflux.example.org
db:
image: postgres:latest
image: postgres:15
container_name: postgres
environment:
- POSTGRES_USER=miniflux
+3 -3
View File
@@ -1,4 +1,3 @@
version: '3.4'
services:
traefik:
image: "traefik:v2.3"
@@ -21,7 +20,8 @@ services:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
depends_on:
- db
db:
condition: service_healthy
expose:
- "8080"
environment:
@@ -37,7 +37,7 @@ services:
- "traefik.http.routers.miniflux.entrypoints=websecure"
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
db:
image: postgres:latest
image: postgres:15
container_name: postgres
environment:
- POSTGRES_USER=miniflux
+31 -31
View File
@@ -20,7 +20,7 @@
"panels": [
{
"collapsed": false,
"datasource": null,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
@@ -104,7 +104,7 @@
"type": "bargauge"
},
{
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
@@ -289,7 +289,7 @@
}
},
{
"datasource": null,
"datasource": "Prometheus",
"description": "",
"fieldConfig": {
"defaults": {
@@ -332,7 +332,7 @@
"pluginVersion": "7.1.5",
"targets": [
{
"expr": "go_memstats_sys_bytes{app=\"miniflux\"}",
"expr": "go_memstats_sys_bytes{job=\"miniflux\"}",
"interval": "",
"legendFormat": "{{ instance }} - Memory Used",
"refId": "A"
@@ -348,7 +348,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
@@ -440,7 +440,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
@@ -529,7 +529,7 @@
},
{
"collapsed": false,
"datasource": null,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
@@ -546,7 +546,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
@@ -590,7 +590,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_memstats_sys_bytes{app=\"miniflux\"}",
"expr": "go_memstats_sys_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -699,7 +699,7 @@
"steppedLine": false,
"targets": [
{
"expr": "process_open_fds{app=\"miniflux\"}",
"expr": "process_open_fds{job=\"miniflux\"}",
"interval": "",
"legendFormat": "{{instance }} - Open File Descriptors",
"refId": "A"
@@ -748,7 +748,7 @@
},
{
"collapsed": false,
"datasource": null,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
@@ -816,7 +816,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_memstats_alloc_bytes{app=\"miniflux\"}",
"expr": "go_memstats_alloc_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
@@ -826,7 +826,7 @@
"step": 4
},
{
"expr": "rate(go_memstats_alloc_bytes_total{app=\"miniflux\"}[30s])",
"expr": "rate(go_memstats_alloc_bytes_total{job=\"miniflux\"}[30s])",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
@@ -836,7 +836,7 @@
"step": 4
},
{
"expr": "go_memstats_stack_inuse_bytes{app=\"miniflux\"}",
"expr": "go_memstats_stack_inuse_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
@@ -846,7 +846,7 @@
"step": 4
},
{
"expr": "go_memstats_heap_inuse_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_inuse_bytes{job=\"miniflux\"}",
"format": "time_series",
"hide": false,
"interval": "",
@@ -904,7 +904,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
@@ -945,13 +945,13 @@
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines{app=\"miniflux\"}",
"expr": "go_goroutines{job=\"miniflux\"}",
"interval": "",
"legendFormat": "{{ instance }} - Goroutines",
"refId": "A"
},
{
"expr": "go_threads{app=\"miniflux\"}",
"expr": "go_threads{job=\"miniflux\"}",
"interval": "",
"legendFormat": "{{ instance }} - OS threads",
"refId": "B"
@@ -1003,7 +1003,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
@@ -1046,7 +1046,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_memstats_stack_inuse_bytes{app=\"miniflux\"}",
"expr": "go_memstats_stack_inuse_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1054,7 +1054,7 @@
"refId": "A"
},
{
"expr": "go_memstats_stack_sys_bytes{app=\"miniflux\"}",
"expr": "go_memstats_stack_sys_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1108,7 +1108,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
@@ -1150,7 +1150,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_memstats_heap_alloc_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_alloc_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1158,7 +1158,7 @@
"refId": "B"
},
{
"expr": "go_memstats_heap_sys_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_sys_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1166,7 +1166,7 @@
"refId": "A"
},
{
"expr": "go_memstats_heap_idle_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_idle_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1174,7 +1174,7 @@
"refId": "C"
},
{
"expr": "go_memstats_heap_inuse_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_inuse_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1182,7 +1182,7 @@
"refId": "D"
},
{
"expr": "go_memstats_heap_released_bytes{app=\"miniflux\"}",
"expr": "go_memstats_heap_released_bytes{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1282,7 +1282,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_gc_duration_seconds{app=\"miniflux\"}",
"expr": "go_gc_duration_seconds{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
@@ -1339,7 +1339,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
@@ -1383,7 +1383,7 @@
"steppedLine": false,
"targets": [
{
"expr": "go_memstats_mallocs_total{app=\"miniflux\"} - go_memstats_frees_total{app=\"miniflux\"}",
"expr": "go_memstats_mallocs_total{job=\"miniflux\"} - go_memstats_frees_total{job=\"miniflux\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -1462,4 +1462,4 @@
"title": "Miniflux",
"uid": "vSaPgcFMk",
"version": 23
}
}
+43
View File
@@ -610,4 +610,47 @@ var migrations = []func(tx *sql.Tx) error{
`)
return
},
func(tx *sql.Tx) (err error) {
_, err = tx.Exec(`
ALTER TABLE integrations ADD COLUMN wallabag_only_url bool default 'f';
`)
return
},
func(tx *sql.Tx) (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) {
sql := `
ALTER TABLE integrations ADD COLUMN matrix_bot_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN matrix_bot_user text default '';
ALTER TABLE integrations ADD COLUMN matrix_bot_password text default '';
ALTER TABLE integrations ADD COLUMN matrix_bot_url text default '';
ALTER TABLE integrations ADD COLUMN matrix_bot_chat_id text default '';
`
_, err = tx.Exec(sql)
return
},
func(tx *sql.Tx) (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) {
_, err = tx.Exec(`
ALTER TABLE entries ADD COLUMN tags text[] default '{}';
`)
return
},
func(tx *sql.Tx) (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;
ALTER TABLE users ALTER COLUMN gesture_nav SET default 'tap';
`
_, err = tx.Exec(sql)
return err
},
}
+5 -3
View File
@@ -15,6 +15,7 @@ import (
"miniflux.app/integration"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/proxy"
"miniflux.app/storage"
"github.com/gorilla/mux"
@@ -22,7 +23,7 @@ import (
// Serve handles Fever API calls.
func Serve(router *mux.Router, store *storage.Storage) {
handler := &handler{store}
handler := &handler{store, router}
sr := router.PathPrefix("/fever").Subrouter()
sr.Use(newMiddleware(store).serve)
@@ -30,7 +31,8 @@ func Serve(router *mux.Router, store *storage.Storage) {
}
type handler struct {
store *storage.Storage
store *storage.Storage
router *mux.Router
}
func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
@@ -308,7 +310,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
FeedID: entry.FeedID,
Title: entry.Title,
Author: entry.Author,
HTML: entry.Content,
HTML: proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content),
URL: entry.URL,
IsSaved: isSaved,
IsRead: isRead,
+33 -19
View File
@@ -1,28 +1,42 @@
module miniflux.app
// +heroku goVersion go1.16
// +heroku goVersion go1.19
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/PuerkitoBio/goquery v1.8.1
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 // indirect
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.6
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.13.0
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d
github.com/stretchr/testify v1.6.1 // indirect
github.com/tdewolff/minify/v2 v2.12.0
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/yuin/goldmark v1.4.13
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
mvdan.cc/xurls/v2 v2.4.0
github.com/lib/pq v1.10.9
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530
github.com/prometheus/client_golang v1.15.1
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0
github.com/tdewolff/minify/v2 v2.12.5
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.8.0
golang.org/x/net v0.9.0
golang.org/x/oauth2 v0.7.0
golang.org/x/term v0.8.0
mvdan.cc/xurls/v2 v2.5.0
)
go 1.16
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/tdewolff/parse/v2 v2.6.5 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)
go 1.19
+67 -602
View File
@@ -1,658 +1,123 @@
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 h1:5vu7C+63KTbsSNnLhrgB98Sqy8MNVSW8FdhkcWA/3Rk=
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/go-dap v0.3.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2 h1:9kYRAm87/M5VL+HWegDGIorBWDiErrZrksLKTJBF2IQ=
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2/go.mod h1:8mKJVJkyMI20ifXXO0zlV3sh3FrtjvltAeBqz38clBE=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
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/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d h1:4Jn2kzSKcpxxwpJdyWxfNWKCYe4Uki2VibkExYmBLiA=
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0 h1:qSaU9YAEIxk/ozcmY1hiauktAYTpbwYIrPdQ0L2E8UM=
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tdewolff/minify/v2 v2.12.0 h1:ZyvMKeciyR3vzJrK/oHyBcSmpttQ/V+ah7qOqTZclaU=
github.com/tdewolff/minify/v2 v2.12.0/go.mod h1:8mvf+KglD7XurfvvFZDUYvVURy6bA/r0oTvmakXMnyg=
github.com/tdewolff/parse/v2 v2.6.1 h1:RIfy1erADkO90ynJWvty8VIkqqKYRzf2iLp8ObG174I=
github.com/tdewolff/parse/v2 v2.6.1/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tdewolff/minify/v2 v2.12.5 h1:s2KDBt/D/3ayE3gcqQF8VIgTmYgkx+btuLvVAeePzZM=
github.com/tdewolff/minify/v2 v2.12.5/go.mod h1:i8QXtVyL7Ddwc4I5gqzvgBqKlTMgMNTbiXaPO4Iqg+A=
github.com/tdewolff/parse/v2 v2.6.5 h1:lYvWBk55GkqKl0JJenGpmrgu/cPHQQ6/Mm1hBGswoGQ=
github.com/tdewolff/parse/v2 v2.6.5/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc=
mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
+100 -8
View File
@@ -21,9 +21,11 @@ import (
"miniflux.app/integration"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/proxy"
mff "miniflux.app/reader/handler"
mfs "miniflux.app/reader/subscription"
"miniflux.app/storage"
"miniflux.app/url"
"miniflux.app/validator"
)
@@ -88,8 +90,10 @@ const (
ParamTitle = "t"
// ParamQuickAdd - name of the parameter for a URL being quick subscribed to
ParamQuickAdd = "quickadd"
// ParamDestination - name fo the parameter for the new name of a tag
// ParamDestination - name of the parameter for the new name of a tag
ParamDestination = "dest"
// ParamContinuation - name of the parameter for callers to pass to receive the next page of results
ParamContinuation = "c"
)
// StreamType represents the possible stream types
@@ -130,6 +134,7 @@ type RequestModifiers struct {
FilterTargets []Stream
Streams []Stream
Count int
Offset int
SortDirection string
StartTime int64
StopTime int64
@@ -185,6 +190,7 @@ func (r RequestModifiers) String() string {
result += fmt.Sprintf(" %v\n", s)
}
result += fmt.Sprintf("Count: %d\n", r.Count)
result += fmt.Sprintf("Offset: %d\n", r.Offset)
result += fmt.Sprintf("Sort Direction: %s\n", r.SortDirection)
result += fmt.Sprintf("Continuation Token: %s\n", r.ContinuationToken)
result += fmt.Sprintf("Start Time: %d\n", r.StartTime)
@@ -243,8 +249,9 @@ func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) {
}
result.Count = request.QueryIntParam(r, ParamStreamMaxItems, 0)
result.StartTime = int64(request.QueryIntParam(r, ParamStreamStartTime, 0))
result.StopTime = int64(request.QueryIntParam(r, ParamStreamStopTime, 0))
result.Offset = request.QueryIntParam(r, ParamContinuation, 0)
result.StartTime = request.QueryInt64Param(r, ParamStreamStartTime, int64(0))
result.StopTime = request.QueryInt64Param(r, ParamStreamStopTime, int64(0))
return result, nil
}
@@ -834,6 +841,20 @@ func (h *handler) streamItemContents(w http.ResponseWriter, r *http.Request) {
categories = append(categories, userStarred)
}
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
proxyOption := config.Opts.ProxyOption()
for i := range entry.Enclosures {
if proxyOption == "all" || proxyOption != "none" && !url.IsHTTPS(entry.Enclosures[i].URL) {
for _, mediaType := range config.Opts.ProxyMediaTypes() {
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
break
}
}
}
}
contentItems[i] = contentItem{
ID: fmt.Sprintf(EntryIDLong, entry.ID),
Title: entry.Title,
@@ -1120,9 +1141,18 @@ func (h *handler) handleReadingListStream(w http.ResponseWriter, r *http.Request
logger.Info("[GoogleReader][ReadingListStreamIDs][ClientIP=%s] xt filter type: %#v", clientIP, s)
}
}
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithLimit(rm.Count)
builder.WithOffset(rm.Offset)
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(rm.SortDirection)
if rm.StartTime > 0 {
builder.AfterDate(time.Unix(rm.StartTime, 0))
}
if rm.StopTime > 0 {
builder.BeforeDate(time.Unix(rm.StopTime, 0))
}
rawEntryIDs, err := builder.GetEntryIDs()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#reading-list] [ClientIP=%s] %v", clientIP, err)
@@ -1134,17 +1164,38 @@ func (h *handler) handleReadingListStream(w http.ResponseWriter, r *http.Request
formattedID := strconv.FormatInt(entryID, 10)
itemRefs = append(itemRefs, itemRef{ID: formattedID})
}
json.OK(w, r, streamIDResponse{itemRefs})
totalEntries, err := builder.CountEntries()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#reading-list] [ClientIP=%s] %v", clientIP, err)
json.ServerError(w, r, err)
return
}
continuation := 0
if len(itemRefs)+rm.Offset < totalEntries {
continuation = len(itemRefs) + rm.Offset
}
json.OK(w, r, streamIDResponse{itemRefs, continuation})
}
func (h *handler) handleStarredStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
clientIP := request.ClientIP(r)
builder := h.store.NewEntryQueryBuilder(rm.UserID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithStarred(true)
builder.WithLimit(rm.Count)
builder.WithOffset(rm.Offset)
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(rm.SortDirection)
if rm.StartTime > 0 {
builder.AfterDate(time.Unix(rm.StartTime, 0))
}
if rm.StopTime > 0 {
builder.BeforeDate(time.Unix(rm.StopTime, 0))
}
rawEntryIDs, err := builder.GetEntryIDs()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#starred] [ClientIP=%s] %v", clientIP, err)
@@ -1156,14 +1207,29 @@ func (h *handler) handleStarredStream(w http.ResponseWriter, r *http.Request, rm
formattedID := strconv.FormatInt(entryID, 10)
itemRefs = append(itemRefs, itemRef{ID: formattedID})
}
json.OK(w, r, streamIDResponse{itemRefs})
totalEntries, err := builder.CountEntries()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#starred] [ClientIP=%s] %v", clientIP, err)
json.ServerError(w, r, err)
return
}
continuation := 0
if len(itemRefs)+rm.Offset < totalEntries {
continuation = len(itemRefs) + rm.Offset
}
json.OK(w, r, streamIDResponse{itemRefs, continuation})
}
func (h *handler) handleReadStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
clientIP := request.ClientIP(r)
builder := h.store.NewEntryQueryBuilder(rm.UserID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithStatus(model.EntryStatusRead)
builder.WithLimit(rm.Count)
builder.WithOffset(rm.Offset)
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(rm.SortDirection)
if rm.StartTime > 0 {
@@ -1184,7 +1250,19 @@ func (h *handler) handleReadStream(w http.ResponseWriter, r *http.Request, rm Re
formattedID := strconv.FormatInt(entryID, 10)
itemRefs = append(itemRefs, itemRef{ID: formattedID})
}
json.OK(w, r, streamIDResponse{itemRefs})
totalEntries, err := builder.CountEntries()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#read] [ClientIP=%s] %v", clientIP, err)
json.ServerError(w, r, err)
return
}
continuation := 0
if len(itemRefs)+rm.Offset < totalEntries {
continuation = len(itemRefs) + rm.Offset
}
json.OK(w, r, streamIDResponse{itemRefs, continuation})
}
func (h *handler) handleFeedStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
@@ -1197,8 +1275,10 @@ func (h *handler) handleFeedStream(w http.ResponseWriter, r *http.Request, rm Re
}
builder := h.store.NewEntryQueryBuilder(rm.UserID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithFeedID(feedID)
builder.WithLimit(rm.Count)
builder.WithOffset(rm.Offset)
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(rm.SortDirection)
if rm.StartTime > 0 {
@@ -1210,7 +1290,7 @@ func (h *handler) handleFeedStream(w http.ResponseWriter, r *http.Request, rm Re
rawEntryIDs, err := builder.GetEntryIDs()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#starred] [ClientIP=%s] %v", clientIP, err)
logger.Error("[GoogleReader][/stream/items/ids#feed] [ClientIP=%s] %v", clientIP, err)
json.ServerError(w, r, err)
return
}
@@ -1219,5 +1299,17 @@ func (h *handler) handleFeedStream(w http.ResponseWriter, r *http.Request, rm Re
formattedID := strconv.FormatInt(entryID, 10)
itemRefs = append(itemRefs, itemRef{ID: formattedID})
}
json.OK(w, r, streamIDResponse{itemRefs})
totalEntries, err := builder.CountEntries()
if err != nil {
logger.Error("[GoogleReader][/stream/items/ids#feed] [ClientIP=%s] %v", clientIP, err)
json.ServerError(w, r, err)
return
}
continuation := 0
if len(itemRefs)+rm.Offset < totalEntries {
continuation = len(itemRefs) + rm.Offset
}
json.OK(w, r, streamIDResponse{itemRefs, continuation})
}
+2 -1
View File
@@ -61,7 +61,8 @@ type itemRef struct {
}
type streamIDResponse struct {
ItemRefs []itemRef `json:"itemRefs"`
ItemRefs []itemRef `json:"itemRefs"`
Continuation int `json:"continuation,omitempty,string"`
}
type tagsResponse struct {
+14 -9
View File
@@ -10,15 +10,7 @@ import (
"strings"
)
func dropIPv6zone(address string) string {
i := strings.IndexByte(address, '%')
if i != -1 {
address = address[:i]
}
return address
}
// FindClientIP returns client real IP address.
// FindClientIP returns the client real IP address based on trusted Reverse-Proxy HTTP headers.
func FindClientIP(r *http.Request) string {
headers := []string{"X-Forwarded-For", "X-Real-Ip"}
for _, header := range headers {
@@ -36,6 +28,11 @@ func FindClientIP(r *http.Request) string {
}
// Fallback to TCP/IP source IP address.
return FindRemoteIP(r)
}
// FindRemoteIP returns remote client IP address.
func FindRemoteIP(r *http.Request) string {
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
remoteIP = r.RemoteAddr
@@ -49,3 +46,11 @@ func FindClientIP(r *http.Request) string {
return remoteIP
}
func dropIPv6zone(address string) string {
i := strings.IndexByte(address, '%')
if i != -1 {
address = address[:i]
}
return address
}
+6 -1
View File
@@ -12,6 +12,8 @@ import (
"net/http"
"strings"
"time"
"miniflux.app/logger"
)
const compressionThreshold = 1024
@@ -88,7 +90,10 @@ func (b *Builder) Write() {
case io.Reader:
// Compression not implemented in this case
b.writeHeaders()
io.Copy(b.w, v)
_, err := io.Copy(b.w, v)
if err != nil {
logger.Error("%v", err)
}
}
}
+15
View File
@@ -26,6 +26,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-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
@@ -38,6 +39,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-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
@@ -72,3 +74,16 @@ func NotFound(w http.ResponseWriter, r *http.Request) {
func Redirect(w http.ResponseWriter, r *http.Request, uri string) {
http.Redirect(w, r, uri, http.StatusFound)
}
// RequestedRangeNotSatisfiable sends a range not satisfiable error to the client.
func RequestedRangeNotSatisfiable(w http.ResponseWriter, r *http.Request, contentRange string) {
logger.Error("[HTTP:Range Not Satisfiable] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusRequestedRangeNotSatisfiable)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithHeader("Content-Range", contentRange)
builder.WithBody("Range Not Satisfiable")
builder.Write()
}
+29
View File
@@ -210,3 +210,32 @@ func TestRedirectResponse(t *testing.T) {
t.Fatalf(`Unexpected redirect location, got %q instead of %q`, actualResult, expectedResult)
}
}
func TestRequestedRangeNotSatisfiable(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
RequestedRangeNotSatisfiable(w, r, "bytes */12777")
})
handler.ServeHTTP(w, r)
resp := w.Result()
defer resp.Body.Close()
expectedStatusCode := http.StatusRequestedRangeNotSatisfiable
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedContentRangeHeader := "bytes */12777"
actualContentRangeHeader := resp.Header.Get("Content-Range")
if actualContentRangeHeader != expectedContentRangeHeader {
t.Fatalf(`Unexpected content range header, got %q instead of %q`, actualContentRangeHeader, expectedContentRangeHeader)
}
}
+14
View File
@@ -9,6 +9,7 @@ import (
"miniflux.app/integration/espial"
"miniflux.app/integration/instapaper"
"miniflux.app/integration/linkding"
"miniflux.app/integration/matrixbot"
"miniflux.app/integration/nunuxkeeper"
"miniflux.app/integration/pinboard"
"miniflux.app/integration/pocket"
@@ -54,6 +55,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
integration.WallabagClientSecret,
integration.WallabagUsername,
integration.WallabagPassword,
integration.WallabagOnlyURL,
)
if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil {
@@ -109,6 +111,18 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
}
}
// PushEntries pushes an entry array to third-party providers during feed refreshes.
func PushEntries(entries model.Entries, integration *model.Integration) {
if integration.MatrixBotEnabled {
logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), integration.UserID)
err := matrixbot.PushEntries(entries, integration.MatrixBotURL, integration.MatrixBotUser, integration.MatrixBotPassword, integration.MatrixBotChatID)
if err != nil {
logger.Error("[Integration] push entries to matrix bot failed: %v", err)
}
}
}
// PushEntry pushes an entry to third-party providers during feed refreshes.
func PushEntry(entry *model.Entry, integration *model.Integration) {
if integration.TelegramBotEnabled {
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2021 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package matrixbot // import "miniflux.app/integration/matrixbot"
import (
"fmt"
"miniflux.app/logger"
"miniflux.app/model"
"github.com/matrix-org/gomatrix"
)
// PushEntry pushes entries to matrix chat using integration settings provided
func PushEntries(entries model.Entries, serverURL, botLogin, botPassword, chatID string) error {
bot, err := gomatrix.NewClient(serverURL, "", "")
if err != nil {
return fmt.Errorf("matrixbot: bot creation failed: %w", err)
}
resp, err := bot.Login(&gomatrix.ReqLogin{
Type: "m.login.password",
User: botLogin,
Password: botPassword,
})
if err != nil {
logger.Debug("matrixbot: login failed: %w", err)
return fmt.Errorf("matrixbot: login failed, please check your credentials or turn on debug mode")
}
bot.SetCredentials(resp.UserID, resp.AccessToken)
defer func() {
bot.Logout()
bot.ClearCredentials()
}()
message := ""
for _, entry := range entries {
message = message + entry.Title + " " + entry.URL + "\n"
}
if _, err = bot.SendText(chatID, message); err != nil {
logger.Debug("matrixbot: sending message failed: %w", err)
return fmt.Errorf("matrixbot: sending message failed, turn on debug mode for more informations")
}
return nil
}
+9 -3
View File
@@ -20,11 +20,12 @@ type Client struct {
clientSecret string
username string
password string
onlyURL bool
}
// NewClient returns a new Wallabag client.
func NewClient(baseURL, clientID, clientSecret, username, password string) *Client {
return &Client{baseURL, clientID, clientSecret, username, password}
func NewClient(baseURL, clientID, clientSecret, username, password string, onlyURL bool) *Client {
return &Client{baseURL, clientID, clientSecret, username, password, onlyURL}
}
// AddEntry sends a link to Wallabag.
@@ -48,9 +49,14 @@ func (c *Client) createEntry(accessToken, link, title, content string) error {
return fmt.Errorf("wallbag: unable to get entries endpoint: %v", err)
}
data := map[string]string{"url": link, "title": title}
if !c.onlyURL {
data["content"] = content
}
clt := client.New(endpoint)
clt.WithAuthorization("Bearer " + accessToken)
response, err := clt.PostJSON(map[string]string{"url": link, "title": title, "content": content})
response, err := clt.PostJSON(data)
if err != nil {
return fmt.Errorf("wallabag: unable to post entry: %v", err)
}
+2
View File
@@ -23,5 +23,7 @@ func AvailableLanguages() map[string]string {
"el_EL": "Ελληνικά",
"fi_FI": "Suomi",
"hi_IN": "हिन्दी",
"uk_UA": "Українська",
"id_ID": "Bahasa Indonesia",
}
}
+20 -24
View File
@@ -53,6 +53,10 @@ var pluralForms = map[string]pluralFormFunc{
return 2
},
// nplurals=1; plural=0;
"id_ID": func(n int) int {
return 0
},
// nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
"pl_PL": func(n int) int {
if n == 1 {
@@ -72,32 +76,24 @@ var pluralForms = map[string]pluralFormFunc{
}
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);
"ru_RU": func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
// nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
"sr_RS": func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
"ru_RU": pluralFormRuSrUa,
"uk_UA": pluralFormRuSrUa,
"sr_RS": pluralFormRuSrUa,
// nplurals=1; plural=0;
"zh_CN": 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);
func pluralFormRuSrUa(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
}
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Artikel nach oben blättern",
"page.keyboard_shortcuts.remove_feed": "Dieses Abonnement entfernen",
"page.keyboard_shortcuts.go_to_search": "Fokus auf das Suchformular setzen",
"page.keyboard_shortcuts.toggle_entry_attachments": "Artikel Anhänge öffnen/schließen",
"page.keyboard_shortcuts.close_modal": "Liste der Tastenkürzel schließen",
"page.users.title": "Benutzer",
"page.users.username": "Benutzername",
@@ -262,7 +263,8 @@
"error.invalid_language": "Ungültige Sprache.",
"error.invalid_timezone": "Ungültige Zeitzone.",
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
"error.invalid_display_mode": "Ungültiger Web-App-Anzeigemodus.",
"error.invalid_display_mode": "Progressive Web App (PWA) Anzeigemodus",
"error.invalid_gesture_nav": "Ungültige Gestennavigation.",
"error.invalid_default_home_page": "Ungültige Standard-Startseite!",
"form.feed.label.title": "Titel",
"form.feed.label.site_url": "Webseite-URL",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Browser",
"form.prefs.select.publish_time": "Eintrag veröffentlichte Zeit",
"form.prefs.select.created_time": "Eintrag erstellt Zeit",
"form.prefs.select.alphabetical": "Alphabetisch",
"form.prefs.select.unread_count": "Ungelesen zählen",
"form.prefs.select.none": "Keiner",
"form.prefs.select.tap": "Doppeltippen",
"form.prefs.select.swipe": "Wischen",
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",
"form.prefs.label.entry_swipe": "Wischgeste für Einträge auf dem Handy aktivieren",
"form.prefs.label.entry_swipe": "Aktivieren Sie das Streichen von Einträgen auf Touchscreens",
"form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen",
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
"form.prefs.label.entry_order": "Eintrag Sortierspalte",
"form.prefs.label.default_home_page": "Standard Startseite",
"form.prefs.label.categories_sorting_order": "Kategorien sortieren",
"form.import.label.file": "OPML Datei",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Fever API aktivieren",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Verbinden Sie Ihr Pocket Konto",
"form.integration.wallabag_activate": "Artikel in Wallabag speichern",
"form.integration.wallabag_only_url": "Nur URL senden (anstelle des vollständigen Inhalts)",
"form.integration.wallabag_endpoint": "Wallabag URL",
"form.integration.wallabag_client_id": "Wallabag Client-ID",
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Artikel in Linkding speichern",
"form.integration.linkding_endpoint": "Linkding API-Endpunkt",
"form.integration.linkding_api_key": "Linkding API-Schlüssel",
"form.integration.matrix_bot_activate": "Neue Artikel in die Matrix übertragen",
"form.integration.matrix_bot_user": "Benutzername für Matrix",
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
"form.integration.matrix_bot_url": "URL des Matrix-Servers",
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
"form.api_key.label.description": "API-Schlüsselbezeichnung",
"form.submit.loading": "Lade...",
"form.submit.saving": "Speichern...",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Μετακινηση στοιχείου στην κορυφή",
"page.keyboard_shortcuts.remove_feed": "Κατάργηση αυτής της ροής",
"page.keyboard_shortcuts.go_to_search": "Ορίστε εστίαση στη φόρμα αναζήτησης",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Κλείσιμο παραθύρου διαλόγου",
"page.users.title": "Χρήστες",
"page.users.username": "Χρήστης",
@@ -241,6 +242,7 @@
"error.invalid_timezone": "Μη έγκυρη ζώνη ώρας.",
"error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.",
"error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.",
"error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.",
"error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!",
"error.empty_file": "Αυτό το αρχείο είναι κενό.",
"error.bad_credentials": "Μη έγκυρο όνομα χρήστη ή κωδικό πρόσβασης.",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Καταχωρήσεις ανά σελίδα",
"form.prefs.label.default_reading_speed": "Ταχύτητα ανάγνωσης άλλων γλωσσών (λέξεις ανά λεπτό)",
"form.prefs.label.cjk_reading_speed": "Ταχύτητα ανάγνωσης για κινέζικα, κορεάτικα και ιαπωνικά (χαρακτήρες ανά λεπτό)",
"form.prefs.label.display_mode": "Λειτουργία προβολής εφαρμογών ιστού (χρειάζεται επανεγκατάσταση)",
"form.prefs.label.display_mode": "Λειτουργία προβολής προοδευτικής εφαρμογής Ιστού (PWA)",
"form.prefs.select.older_first": "Παλαιότερες καταχωρήσεις πρώτα",
"form.prefs.select.recent_first": "Πρόσφατες καταχωρήσεις πρώτα",
"form.prefs.select.fullscreen": "Πλήρης οθόνη",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Περιηγητής",
"form.prefs.select.publish_time": "Δημοσιευμένος χρόνος εισόδου",
"form.prefs.select.created_time": "Χρόνος δημιουργίας καταχώρησης",
"form.prefs.select.alphabetical": "Αλφαβητική σειρά",
"form.prefs.select.unread_count": "Αριθμός μη αναγνωσμένων",
"form.prefs.select.none": "Κανένας",
"form.prefs.select.tap": "Διπλό χτύπημα",
"form.prefs.select.swipe": "Σουφρώνω",
"form.prefs.label.keyboard_shortcuts": "Ενεργοποίηση συντομεύσεων πληκτρολογίου",
"form.prefs.label.entry_swipe": "Ενεργοποιήστε τη χειρονομία σάρωσης στις καταχωρήσεις στο κινητό",
"form.prefs.label.entry_swipe": "Ενεργοποιήστε το σάρωση καταχώρισης στις οθόνες αφής",
"form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
"form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
"form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
"form.import.label.file": "Αρχείο OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Ενεργοποιήστε το Fever API",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Συνδέστε τον λογαριασμό Pocket σας",
"form.integration.wallabag_activate": "Αποθήκευση άρθρων στο Wallabag",
"form.integration.wallabag_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
"form.integration.wallabag_endpoint": "Τελικό σημείο Wallabag API ",
"form.integration.wallabag_client_id": "Ταυτότητα πελάτη Wallabag",
"form.integration.wallabag_client_secret": "Wallabag Μυστικό Πελάτη",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Αποθήκευση άρθρων στο Linkding",
"form.integration.linkding_endpoint": "Τελικό σημείο Linkding API",
"form.integration.linkding_api_key": "Κλειδί API Linkding",
"form.integration.matrix_bot_activate": "Μεταφορά νέων άρθρων στο Matrix",
"form.integration.matrix_bot_user": "Όνομα χρήστη για το Matrix",
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
"form.integration.matrix_bot_url": "URL διακομιστή Matrix",
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
"form.api_key.label.description": "Ετικέτα κλειδιού API",
"form.submit.loading": "Φόρτωση...",
"form.submit.saving": "Αποθήκευση...",
+52 -37
View File
@@ -2,7 +2,7 @@
"confirm.question": "Are you sure?",
"confirm.yes": "yes",
"confirm.no": "no",
"confirm.loading": "In progress...",
"confirm.loading": "In progress",
"action.subscribe": "Subscribe",
"action.save": "Save",
"action.or": "or",
@@ -40,7 +40,7 @@
"menu.refresh_all_feeds": "Refresh all feeds in the background",
"menu.edit_feed": "Edit",
"menu.edit_category": "Edit",
"menu.add_feed": "Add subscription",
"menu.add_feed": "Add feed",
"menu.add_user": "Add user",
"menu.flush_history": "Flush history",
"menu.feed_entries": "Entries",
@@ -48,7 +48,7 @@
"menu.create_api_key": "Create a new API key",
"menu.shared_entries": "Shared entries",
"search.label": "Search",
"search.placeholder": "Search...",
"search.placeholder": "Search",
"pagination.next": "Next",
"pagination.previous": "Previous",
"entry.status.unread": "Unread",
@@ -60,12 +60,12 @@
"entry.bookmark.toggle.off": "Unstar",
"entry.bookmark.toast.on": "Starred",
"entry.bookmark.toast.off": "Unstarred",
"entry.state.saving": "Saving...",
"entry.state.loading": "Loading...",
"entry.state.saving": "Saving",
"entry.state.loading": "Loading",
"entry.save.label": "Save",
"entry.save.title": "Save this article",
"entry.save.title": "Save this entry",
"entry.save.completed": "Done!",
"entry.save.toast.completed": "Article saved",
"entry.save.toast.completed": "Entry saved",
"entry.scraper.label": "Download",
"entry.scraper.title": "Fetch original content",
"entry.scraper.completed": "Done!",
@@ -73,7 +73,7 @@
"entry.comments.label": "Comments",
"entry.comments.title": "View Comments",
"entry.share.label": "Share",
"entry.share.title": "Share this article",
"entry.share.title": "Share this entry",
"entry.unshare.label": "Unshare",
"entry.shared_entry.title": "Open the public link",
"entry.shared_entry.label": "Share",
@@ -81,13 +81,13 @@
"%d minute read",
"%d minutes read"
],
"page.shared_entries.title": "Shared Entries",
"page.shared_entries.title": "Shared entries",
"page.unread.title": "Unread",
"page.starred.title": "Starred",
"page.categories.title": "Categories",
"page.categories.no_feed": "No feed.",
"page.categories.entries": "Articles",
"page.categories.feeds": "Subscriptions",
"page.categories.entries": "Entries",
"page.categories.feeds": "Feeds",
"page.categories.feed_count": [
"There is %d feed.",
"There are %d feeds."
@@ -117,12 +117,12 @@
"page.about.global_config_options": "Global configuration options",
"page.about.postgres_version": "Postgres version:",
"page.about.go_version": "Go version:",
"page.add_feed.title": "New Subscription",
"page.add_feed.title": "New feed",
"page.add_feed.no_category": "There is no category. You must have at least one category.",
"page.add_feed.label.url": "URL",
"page.add_feed.submit": "Find a subscription",
"page.add_feed.submit": "Find a feed",
"page.add_feed.legend.advanced_options": "Advanced Options",
"page.add_feed.choose_feed": "Choose a Subscription",
"page.add_feed.choose_feed": "Choose a feed",
"page.edit_feed.title": "Edit Feed: %s",
"page.edit_feed.last_check": "Last check:",
"page.edit_feed.last_modified_header": "LastModified header:",
@@ -158,10 +158,11 @@
"page.keyboard_shortcuts.mark_page_as_read": "Mark current page as read",
"page.keyboard_shortcuts.download_content": "Download original content",
"page.keyboard_shortcuts.toggle_bookmark_status": "Toggle bookmark",
"page.keyboard_shortcuts.save_article": "Save article",
"page.keyboard_shortcuts.save_article": "Save entry",
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll item to top",
"page.keyboard_shortcuts.remove_feed": "Remove this feed",
"page.keyboard_shortcuts.go_to_search": "Set focus on search form",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Close modal dialog",
"page.users.title": "Users",
"page.users.username": "Username",
@@ -209,20 +210,20 @@
"alert.no_shared_entry": "There is no shared entry.",
"alert.no_bookmark": "There is no bookmark at the moment.",
"alert.no_category": "There is no category.",
"alert.no_category_entry": "There are no articles in this category.",
"alert.no_feed_entry": "There are no articles for this feed.",
"alert.no_feed": "You don't have any subscriptions.",
"alert.no_feed_in_category": "There is no subscription for this category.",
"alert.no_category_entry": "There are no entries in this category.",
"alert.no_feed_entry": "There are no entries for this feed.",
"alert.no_feed": "You dont have any feeds.",
"alert.no_feed_in_category": "There is no feed for this category.",
"alert.no_history": "There is no history at the moment.",
"alert.feed_error": "There is a problem with this feed",
"alert.no_search_result": "There are no results for this search.",
"alert.no_unread_entry": "There are no unread articles.",
"alert.no_unread_entry": "There are no unread entries.",
"alert.no_user": "You are the only user.",
"alert.account_unlinked": "Your external account is now dissociated!",
"alert.account_linked": "Your external account is now linked!",
"alert.pocket_linked": "Your Pocket account is now linked!",
"alert.prefs_saved": "Preferences saved!",
"error.unlink_account_without_password": "You must define a password otherwise you won't be able to login again.",
"error.unlink_account_without_password": "You must define a password otherwise you wont be able to login again.",
"error.duplicate_linked_account": "There is already someone associated with this provider!",
"error.duplicate_fever_username": "There is already someone else with the same Fever username!",
"error.duplicate_googlereader_username": "There is already someone else with the same Google Reader username!",
@@ -235,12 +236,13 @@
"error.unable_to_create_user": "Unable to create this user.",
"error.unable_to_update_user": "Unable to update this user.",
"error.unable_to_update_feed": "Unable to update this feed.",
"error.subscription_not_found": "Unable to find any subscription.",
"error.subscription_not_found": "Unable to find any feed.",
"error.invalid_theme": "Invalid theme.",
"error.invalid_language": "Invalid language.",
"error.invalid_timezone": "Invalid timezone.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_display_mode": "Invalid web app display mode.",
"error.invalid_gesture_nav": "Invalid gesture navigation.",
"error.invalid_default_home_page": "Invalid default homepage!",
"error.empty_file": "This file is empty.",
"error.bad_credentials": "Invalid username or password.",
@@ -292,11 +294,11 @@
"form.prefs.label.language": "Language",
"form.prefs.label.timezone": "Timezone",
"form.prefs.label.theme": "Theme",
"form.prefs.label.entry_sorting": "Entry Sorting",
"form.prefs.label.entry_sorting": "Entry sorting",
"form.prefs.label.entries_per_page": "Entries per page",
"form.prefs.label.default_reading_speed": "Reading speed for other languages (words per minute)",
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
"form.prefs.label.display_mode": "Web app display mode (needs reinstalling)",
"form.prefs.label.display_mode": "Progressive Web App (PWA) display mode",
"form.prefs.select.older_first": "Older entries first",
"form.prefs.select.recent_first": "Recent entries first",
"form.prefs.select.fullscreen": "Fullscreen",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Browser",
"form.prefs.select.publish_time": "Entry published time",
"form.prefs.select.created_time": "Entry created time",
"form.prefs.select.alphabetical": "Alphabetical",
"form.prefs.select.unread_count": "Unread count",
"form.prefs.select.none": "None",
"form.prefs.select.tap": "Double tap",
"form.prefs.select.swipe": "Swipe",
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
"form.prefs.label.entry_swipe": "Enable swipe gesture on entries on mobile",
"form.prefs.label.show_reading_time": "Show estimated reading time for articles",
"form.prefs.label.entry_swipe": "Enable entry swipe on touch screens",
"form.prefs.label.gesture_nav": "Gesture to navigate between entries",
"form.prefs.label.show_reading_time": "Show estimated reading time for entries",
"form.prefs.label.custom_css": "Custom CSS",
"form.prefs.label.entry_order": "Entry Sorting Column",
"form.prefs.label.entry_order": "Entry sorting column",
"form.prefs.label.default_home_page": "Default home page",
"form.prefs.label.categories_sorting_order": "Categories sorting",
"form.import.label.file": "OPML file",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Activate Fever API",
@@ -321,39 +330,45 @@
"form.integration.googlereader_username": "Google Reader Username",
"form.integration.googlereader_password": "Google Reader Password",
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
"form.integration.pinboard_activate": "Save articles to Pinboard",
"form.integration.pinboard_activate": "Save entries to Pinboard",
"form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Pinboard Tags",
"form.integration.pinboard_bookmark": "Mark bookmark as unread",
"form.integration.instapaper_activate": "Save articles to Instapaper",
"form.integration.instapaper_activate": "Save entries to Instapaper",
"form.integration.instapaper_username": "Instapaper Username",
"form.integration.instapaper_password": "Instapaper Password",
"form.integration.pocket_activate": "Save articles to Pocket",
"form.integration.pocket_activate": "Save entries to Pocket",
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Connect your Pocket account",
"form.integration.wallabag_activate": "Save articles to Wallabag",
"form.integration.wallabag_activate": "Save entries to Wallabag",
"form.integration.wallabag_only_url": "Send only URL (instead of full content)",
"form.integration.wallabag_endpoint": "Wallabag API Endpoint",
"form.integration.wallabag_client_id": "Wallabag Client ID",
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
"form.integration.wallabag_username": "Wallabag Username",
"form.integration.wallabag_password": "Wallabag Password",
"form.integration.nunux_keeper_activate": "Save articles to Nunux Keeper",
"form.integration.nunux_keeper_activate": "Save entries to Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
"form.integration.espial_activate": "Save articles to Espial",
"form.integration.espial_activate": "Save entries to Espial",
"form.integration.espial_endpoint": "Espial API Endpoint",
"form.integration.espial_api_key": "Espial API key",
"form.integration.espial_tags": "Espial Tags",
"form.integration.telegram_bot_activate": "Push new articles to Telegram chat",
"form.integration.telegram_bot_activate": "Push new entries to Telegram chat",
"form.integration.telegram_bot_token": "Bot token",
"form.integration.telegram_chat_id": "Chat ID",
"form.integration.linkding_activate": "Save articles to Linkding",
"form.integration.linkding_activate": "Save entries to Linkding",
"form.integration.linkding_endpoint": "Linkding API Endpoint",
"form.integration.linkding_api_key": "Linkding API key",
"form.integration.matrix_bot_activate": "Push new entries to Matrix",
"form.integration.matrix_bot_user": "Username for Matrix",
"form.integration.matrix_bot_password": "Password for Matrix user",
"form.integration.matrix_bot_url": "Matrix server URL",
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Loading...",
"form.submit.saving": "Saving...",
"form.submit.loading": "Loading",
"form.submit.saving": "Saving",
"time_elapsed.not_yet": "not yet",
"time_elapsed.yesterday": "yesterday",
"time_elapsed.now": "just now",
+86 -71
View File
@@ -1,5 +1,5 @@
{
"confirm.question": "Estás seguro?",
"confirm.question": "¿Estás seguro?",
"confirm.yes": "sí",
"confirm.no": "no",
"confirm.loading": "En progreso...",
@@ -21,7 +21,7 @@
"menu.starred": "Marcadores",
"menu.history": "Historial",
"menu.feeds": "Fuentes",
"menu.categories": "Categorias",
"menu.categories": "Categorías",
"menu.settings": "Configuración",
"menu.logout": "Cerrar sesión",
"menu.preferences": "Preferencias",
@@ -32,21 +32,21 @@
"menu.export": "Exportar",
"menu.import": "Importar",
"menu.create_category": "Crear una categoría",
"menu.mark_page_as_read": "Marcar esta pagína como leída",
"menu.mark_page_as_read": "Marcar esta página como leída",
"menu.mark_all_as_read": "Marcar todos como leídos",
"menu.show_all_entries": "Mostrar todas las entradas",
"menu.show_only_unread_entries": "Mostrar solo las entradas no leídas",
"menu.show_all_entries": "Mostrar todos los artículos",
"menu.show_only_unread_entries": "Mostrar solo los artículos no leídos",
"menu.refresh_feed": "Refrescar",
"menu.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
"menu.edit_feed": "Editar",
"menu.edit_category": "Editar",
"menu.add_feed": "Agregar suscripción",
"menu.add_feed": "Agregar fuente",
"menu.add_user": "Agregar usuario",
"menu.flush_history": "Borrar historial",
"menu.feed_entries": "Artículos",
"menu.api_keys": "Claves API",
"menu.create_api_key": "Crear una nueva clave API",
"menu.shared_entries": "Entradas compartidas",
"menu.shared_entries": "Artículos compartidos",
"search.label": "Buscar",
"search.placeholder": "Búsqueda...",
"pagination.next": "Siguiente",
@@ -55,7 +55,7 @@
"entry.status.read": "Leído",
"entry.status.toast.unread": "Marcado como no leído",
"entry.status.toast.read": "Marcado como leído",
"entry.status.title": "Cambiar estado de entrada",
"entry.status.title": "Cambiar estado del artículo",
"entry.bookmark.toggle.on": "Marcar",
"entry.bookmark.toggle.off": "Desmarcar",
"entry.bookmark.toast.on": "Sembrado de estrellas",
@@ -63,17 +63,17 @@
"entry.state.saving": "Guardando...",
"entry.state.loading": "Cargando...",
"entry.save.label": "Guardar",
"entry.save.title": "Guardar este articulo",
"entry.save.title": "Guardar este artículo",
"entry.save.completed": "¡Hecho!",
"entry.save.toast.completed": "Artículo guardado",
"entry.save.toast.completed": "Artículos guardados",
"entry.scraper.label": "Descargar",
"entry.scraper.title": "Obtener contenido original",
"entry.scraper.completed": "¡Hecho!",
"entry.external_link.label": "Enlace externo",
"entry.comments.label": "Comentarios",
"entry.comments.title": "Ver comentarios",
"entry.share.label": "Comparta",
"entry.share.title": "Comparta este articulo",
"entry.share.label": "Compartir",
"entry.share.title": "Compartir este artículo",
"entry.unshare.label": "No compartir",
"entry.shared_entry.title": "Abrir el enlace público",
"entry.shared_entry.label": "Compartir",
@@ -81,26 +81,26 @@
"%d minuto de lectura",
"%d minutos de lectura"
],
"page.shared_entries.title": "Entradas compartidas",
"page.shared_entries.title": "Artículos compartidos",
"page.unread.title": "No leídos",
"page.starred.title": "Marcadores",
"page.categories.title": "Categorias",
"page.categories.no_feed": "No fuente.",
"page.categories.title": "Categorías",
"page.categories.no_feed": "Sin fuente.",
"page.categories.entries": "Artículos",
"page.categories.feeds": "Suscripciones",
"page.categories.feeds": "Fuentes",
"page.categories.feed_count": [
"Hay %d fuente.",
"Hay %d fuentes."
],
"page.categories.unread_counter": "Número de entradas no leídas",
"page.categories.unread_counter": "Número de artículos no leídos",
"page.new_category.title": "Nueva categoría",
"page.new_user.title": "Nuevo usario",
"page.new_user.title": "Nuevo usuario",
"page.edit_category.title": "Editar categoría: %s",
"page.edit_user.title": "Editar usuario: %s",
"page.feeds.title": "Fuentes",
"page.feeds.last_check": "Última verificación:",
"page.feeds.unread_counter": "Número de entradas no leídas",
"page.feeds.read_counter": "Número de entradas leídas",
"page.feeds.unread_counter": "Número de artículos no leídos",
"page.feeds.read_counter": "Número de artículos leídos",
"page.feeds.error_count": [
"%d error",
"%d errores"
@@ -109,7 +109,7 @@
"page.import.title": "Importar",
"page.search.title": "Resultados de la búsqueda",
"page.about.title": "Acerca de",
"page.about.credits": "Creditos",
"page.about.credits": "Créditos",
"page.about.version": "Versión:",
"page.about.build_date": "Fecha de construcción:",
"page.about.author": "Autor:",
@@ -117,12 +117,12 @@
"page.about.global_config_options": "Opciones de configuración global",
"page.about.postgres_version": "Postgres versión:",
"page.about.go_version": "Go versión:",
"page.add_feed.title": "Nueva suscripción",
"page.add_feed.title": "Nueva fuente",
"page.add_feed.no_category": "No hay categoría. Debe tener al menos una categoría.",
"page.add_feed.label.url": "URL",
"page.add_feed.submit": "Encontrar una suscripción",
"page.add_feed.submit": "Encontrar una fuente",
"page.add_feed.legend.advanced_options": "Opciones avanzadas",
"page.add_feed.choose_feed": "Elegir una suscripción",
"page.add_feed.choose_feed": "Elegir una fuente",
"page.edit_feed.title": "Editar fuente: %s",
"page.edit_feed.last_check": "Última verificación:",
"page.edit_feed.last_modified_header": "Cabecera de LastModified:",
@@ -139,14 +139,14 @@
"page.keyboard_shortcuts.go_to_starred": "Ir a los marcadores",
"page.keyboard_shortcuts.go_to_history": "Ir al historial",
"page.keyboard_shortcuts.go_to_feeds": "Ir a las fuentes",
"page.keyboard_shortcuts.go_to_categories": "Ir a las categorias",
"page.keyboard_shortcuts.go_to_categories": "Ir a las categorías",
"page.keyboard_shortcuts.go_to_settings": "Ir a la configuración",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Mostrar atajos de teclado",
"page.keyboard_shortcuts.go_to_previous_item": "Ir al elemento anterior",
"page.keyboard_shortcuts.go_to_next_item": "Ir al elemento siguiente",
"page.keyboard_shortcuts.go_to_feed": "Ir a la fuente",
"page.keyboard_shortcuts.go_to_previous_page": "Ir al pagína anterior",
"page.keyboard_shortcuts.go_to_next_page": "Ir al pagína siguiente",
"page.keyboard_shortcuts.go_to_previous_page": "Ir al página anterior",
"page.keyboard_shortcuts.go_to_next_page": "Ir al página siguiente",
"page.keyboard_shortcuts.open_item": "Abrir el elemento seleccionado",
"page.keyboard_shortcuts.open_original": "Abrir el enlace original",
"page.keyboard_shortcuts.open_original_same_window": "Abrir enlace original en la pestaña actual",
@@ -155,13 +155,14 @@
"page.keyboard_shortcuts.toggle_read_status_next": "Marcar como leído o no leído, enfoque siguiente",
"page.keyboard_shortcuts.toggle_read_status_prev": "Marcar como leído o no leído, foco anterior",
"page.keyboard_shortcuts.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
"page.keyboard_shortcuts.mark_page_as_read": "Marcar pagína actual como leída",
"page.keyboard_shortcuts.mark_page_as_read": "Marcar página actual como leída",
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
"page.keyboard_shortcuts.save_article": "Guardar artículo",
"page.keyboard_shortcuts.scroll_item_to_top": "Desplazar elemento hacia arriba",
"page.keyboard_shortcuts.remove_feed": "Quitar esta fuente",
"page.keyboard_shortcuts.go_to_search": "Centrarse en el cuadro de búsqueda",
"page.keyboard_shortcuts.toggle_entry_attachments": "Alternar abrir/cerrar adjuntos de la entrada",
"page.keyboard_shortcuts.close_modal": "Cerrar el cuadro de diálogo modal",
"page.users.title": "Usuarios",
"page.users.username": "Nombre de usuario",
@@ -185,7 +186,7 @@
"page.integration.miniflux_api_username": "Nombre de usuario",
"page.integration.miniflux_api_password": "Contraseña",
"page.integration.miniflux_api_password_value": "Contraseña de tu cuenta",
"page.integration.bookmarklet": "Bookmarklet",
"page.integration.bookmarklet": "Marcapáginas",
"page.integration.bookmarklet.name": "Agregar a Miniflux",
"page.integration.bookmarklet.instructions": "Arrastrar y soltar este enlace a tus marcadores del navegador.",
"page.integration.bookmarklet.help": "Este enlace especial te permite suscribirte a un sitio de web directamente usando un marcador del navegador.",
@@ -206,18 +207,18 @@
"page.offline.title": "Modo offline",
"page.offline.message": "Estas desconectado",
"page.offline.refresh_page": "Intenta actualizar la página",
"alert.no_shared_entry": "No hay entrada compartida.",
"alert.no_shared_entry": "No hay artículos compartidos.",
"alert.no_bookmark": "No hay marcador en este momento.",
"alert.no_category": "No hay categoría.",
"alert.no_category_entry": "No hay artículos en esta categoria.",
"alert.no_category_entry": "No hay artículos en esta categoría.",
"alert.no_feed_entry": "No hay artículos para esta fuente.",
"alert.no_feed": "No tienes suscripciones.",
"alert.no_feed_in_category": "No hay suscripción para esta categoría.",
"alert.no_feed": "No tienes fuentes.",
"alert.no_feed_in_category": "No hay fuentes para esta categoría.",
"alert.no_history": "No hay historial en este momento.",
"alert.feed_error": "Hay un problema con esta fuente.",
"alert.no_search_result": "No hay resultados para esta búsqueda.",
"alert.no_unread_entry": "No hay artículos sin leer.",
"alert.no_user": "Eres el unico usuario.",
"alert.no_user": "Eres el único usuario.",
"alert.account_unlinked": "¡Tu cuenta externa ya está desvinculada!",
"alert.account_linked": "¡Tu cuenta externa ya está vinculada!",
"alert.pocket_linked": "¡Tu cuenta de Pocket ya está vinculada!",
@@ -235,7 +236,7 @@
"error.unable_to_create_user": "Incapaz de crear este usuario.",
"error.unable_to_update_user": "Incapaz de actualizar este usuario.",
"error.unable_to_update_feed": "Incapaz de actualizar esta fuente.",
"error.subscription_not_found": "Incapaz de encontrar ninguna suscripción.",
"error.subscription_not_found": "Incapaz de encontrar alguna fuente.",
"error.empty_file": "Este archivo está vacío.",
"error.bad_credentials": "Usuario o contraseña no válido.",
"error.fields_mandatory": "Todos los campos son obligatorios.",
@@ -244,7 +245,7 @@
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
"error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
"error.entries_per_page_invalid": "El número de entradas por página no es válido.",
"error.entries_per_page_invalid": "El número de artículos por página no es válido.",
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
"error.feed_already_exists": "Este feed ya existe.",
"error.invalid_feed_url": "URL de feed no válida.",
@@ -261,30 +262,31 @@
"error.invalid_theme": "Tema no válido.",
"error.invalid_language": "Idioma no válido.",
"error.invalid_timezone": "Zona horaria no válida.",
"error.invalid_entry_direction": "Dirección de entrada no válida.",
"error.invalid_entry_direction": "Dirección de artículo no válida.",
"error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.",
"error.invalid_default_home_page": "¡Página de inicio por defecto inválida!",
"error.invalid_gesture_nav": "Navegación por gestos no válida.",
"error.invalid_default_home_page": "¡Página de inicio por defecto no válida!",
"form.feed.label.title": "Título",
"form.feed.label.site_url": "URL del sitio",
"form.feed.label.feed_url": "URL de la fuente",
"form.feed.label.category": "Categoría",
"form.feed.label.crawler": "Obtener contento original",
"form.feed.label.feed_username": "Nombre de usuario de fuente",
"form.feed.label.feed_password": "Contraseña de fuente",
"form.feed.label.crawler": "Obtener rastreador original",
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
"form.feed.label.feed_password": "Contraseña de la fuente",
"form.feed.label.user_agent": "Invalidar el agente de usuario predeterminado",
"form.feed.label.cookie": "Configurar las cookies",
"form.feed.label.scraper_rules": "Reglas de raspador",
"form.feed.label.scraper_rules": "Reglas de extracción de información",
"form.feed.label.rewrite_rules": "Reglas de reescribir",
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado(reescritura)",
"form.feed.label.blocklist_rules": "Reglas de Filtrado (Bloquear)",
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado (Reescritura)",
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
"form.feed.label.disabled": "No actualice este feed",
"form.feed.label.hide_globally": "Ocultar entradas en la lista global de no leídos",
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
"form.category.label.title": "Título",
"form.category.hide_globally": "Ocultar entradas en la lista global de no leídos",
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
"form.user.label.username": "Nombre de usuario",
"form.user.label.password": "Contraseña",
"form.user.label.confirmation": "Confirmación de contraseña",
@@ -292,65 +294,78 @@
"form.prefs.label.language": "Idioma",
"form.prefs.label.timezone": "Zona horaria",
"form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Clasificación de entradas",
"form.prefs.label.entries_per_page": "Entradas por página",
"form.prefs.label.entry_sorting": "Clasificación de artículos",
"form.prefs.label.entries_per_page": "Artículos por página",
"form.prefs.label.default_reading_speed": "Velocidad de lectura de otras lenguas (palabras por minuto)",
"form.prefs.label.cjk_reading_speed": "Velocidad de lectura en chino, coreano y japonés (caracteres por minuto)",
"form.prefs.label.display_mode": "Modo de visualización de la aplicación web (necesita reinstalación)",
"form.prefs.select.older_first": "Entradas más viejas primero",
"form.prefs.select.recent_first": "Entradas recientes primero",
"form.prefs.label.display_mode": "Modo de visualización de aplicación web progresiva (PWA)",
"form.prefs.select.older_first": "Artículos antiguos primero",
"form.prefs.select.recent_first": "Artículos recientes primero",
"form.prefs.select.fullscreen": "Pantalla completa",
"form.prefs.select.standalone": "Ser único",
"form.prefs.select.standalone": "Autónomo",
"form.prefs.select.minimal_ui": "Mínimo",
"form.prefs.select.browser": "Navegador",
"form.prefs.select.publish_time": "Hora de publicación de la entrada",
"form.prefs.select.created_time": "Hora de creación de la entrada",
"form.prefs.select.publish_time": "Hora de publicación del artículo",
"form.prefs.select.created_time": "Hora de creación del artículo",
"form.prefs.select.alphabetical": "Alfabético",
"form.prefs.select.unread_count": "Recuento de no leídos",
"form.prefs.select.none": "Ninguno",
"form.prefs.select.tap": "Doble toque",
"form.prefs.select.swipe": "Golpe fuerte",
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
"form.prefs.label.entry_swipe": "Habilitar el gesto de deslizar el dedo en las entradas en el móvil",
"form.prefs.label.entry_swipe": "Habilitar deslizamiento de entrada en pantallas táctiles",
"form.prefs.label.gesture_nav": "Gesto para navegar entre entradas",
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
"form.prefs.label.custom_css": "CSS personalizado",
"form.prefs.label.entry_order": "Columna de clasificación de entradas",
"form.prefs.label.entry_order": "Columna de clasificación de artículos",
"form.prefs.label.default_home_page": "Página de inicio por defecto",
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
"form.import.label.file": "Archivo OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Activar API de Fever",
"form.integration.fever_username": "Nombre de usuario de Fever",
"form.integration.fever_password": "Contraseña de Fever",
"form.integration.fever_endpoint": "Extremo de API de Fever:",
"form.integration.fever_endpoint": "Acceso API de Fever:",
"form.integration.googlereader_activate": "Activar API de Google Reader",
"form.integration.googlereader_username": "Nombre de usuario de Google Reader",
"form.integration.googlereader_password": "Contraseña de Google Reader",
"form.integration.googlereader_endpoint": "Extremo de API de Google Reader:",
"form.integration.pinboard_activate": "Guardar artículos a Pinboard",
"form.integration.googlereader_endpoint": "Acceso API de Google Reader:",
"form.integration.pinboard_activate": "Enviar artículos a Pinboard",
"form.integration.pinboard_token": "Token de API de Pinboard",
"form.integration.pinboard_tags": "Etiquetas de Pinboard",
"form.integration.pinboard_bookmark": "Marcar marcador como no leído",
"form.integration.instapaper_activate": "Guardar artículos a Instapaper",
"form.integration.instapaper_activate": "Enviar artículos a Instapaper",
"form.integration.instapaper_username": "Nombre de usuario de Instapaper",
"form.integration.instapaper_password": "Contraseña de Instapaper",
"form.integration.pocket_activate": "Guardar artículos a Pocket",
"form.integration.pocket_activate": "Enviar artículos a Pocket",
"form.integration.pocket_consumer_key": "Clave del consumidor de Pocket",
"form.integration.pocket_access_token": "Token de acceso de Pocket",
"form.integration.pocket_connect_link": "Conectar a la cuenta de Pocket",
"form.integration.wallabag_activate": "Guardar artículos a Wallabag",
"form.integration.wallabag_endpoint": "Extremo de API de Wallabag",
"form.integration.wallabag_activate": "Enviar artículos a Wallabag",
"form.integration.wallabag_only_url": "Enviar solo URL (en lugar de contenido completo)",
"form.integration.wallabag_endpoint": "Acceso API de Wallabag",
"form.integration.wallabag_client_id": "ID de cliente de Wallabag",
"form.integration.wallabag_client_secret": "Secreto cliente de Wallabag",
"form.integration.wallabag_client_secret": "Secreto de cliente de Wallabag",
"form.integration.wallabag_username": "Nombre de usuario de Wallabag",
"form.integration.wallabag_password": "Contraseña de Wallabag",
"form.integration.nunux_keeper_activate": "Guardar artículos a Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Extremo de API de Nunux Keeper",
"form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
"form.integration.espial_activate": "Guardar artículos a Espial",
"form.integration.espial_endpoint": "Extremo de API de Espial",
"form.integration.espial_activate": "Enviar artículos a Espial",
"form.integration.espial_endpoint": "Acceso API de Espial",
"form.integration.espial_api_key": "Clave de API de Espial",
"form.integration.espial_tags": "Etiquetas de Espial",
"form.integration.telegram_bot_activate": "Envíe nuevos artículos al chat de Telegram",
"form.integration.telegram_bot_token": "Token de bot",
"form.integration.telegram_chat_id": "ID de chat",
"form.integration.linkding_activate": "Guardar artículos a Linkding",
"form.integration.linkding_endpoint": "Extremo de API de Linkding",
"form.integration.linkding_activate": "Enviar artículos a Linkding",
"form.integration.linkding_endpoint": "Acceso API de Linkding",
"form.integration.linkding_api_key": "Clave de API de Linkding",
"form.integration.matrix_bot_activate": "Transferir nuevos artículos a Matrix",
"form.integration.matrix_bot_user": "Nombre de usuario para Matrix",
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
"form.integration.matrix_bot_url": "URL del servidor de Matrix",
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
"form.api_key.label.description": "Etiqueta de clave API",
"form.submit.loading": "Cargando...",
"form.submit.saving": "Guardando...",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Vieritä ylös",
"page.keyboard_shortcuts.remove_feed": "Poista tämä syöte",
"page.keyboard_shortcuts.go_to_search": "Aseta painopiste hakukenttään",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Sulje modaalinen valintaikkuna",
"page.users.title": "Käyttäjät",
"page.users.username": "Käyttäjätunnus",
@@ -241,6 +242,7 @@
"error.invalid_timezone": "Virheellinen aikavyöhyke.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.",
"error.invalid_gesture_nav": "Virheellinen ele-navigointi.",
"error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
"error.empty_file": "Tiedosto on tyhjä.",
"error.bad_credentials": "Virheellinen käyttäjänimi tai salasana.",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Artikkelia sivulla",
"form.prefs.label.default_reading_speed": "Muiden kielten lukunopeus (sanaa minuutissa)",
"form.prefs.label.cjk_reading_speed": "Kiinan, Korean ja Japanin lukunopeus (merkkejä minuutissa)",
"form.prefs.label.display_mode": "Verkkosovelluksen näyttötila (vaatii uudelleenasennuksen)",
"form.prefs.label.display_mode": "Progressive Web App (PWA) -näyttötila",
"form.prefs.select.older_first": "Vanhin ensin",
"form.prefs.select.recent_first": "Uusin ensin",
"form.prefs.select.fullscreen": "Kokoruututila",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Selain",
"form.prefs.select.publish_time": "Julkaisuaika",
"form.prefs.select.created_time": "Luomisaika",
"form.prefs.select.alphabetical": "Aakkosjärjestys",
"form.prefs.select.unread_count": "Lukemattomien määrä",
"form.prefs.select.none": "Ei mitään",
"form.prefs.select.tap": "Kaksoisnapauta",
"form.prefs.select.swipe": "Pyyhkäise",
"form.prefs.label.keyboard_shortcuts": "Ota pikanäppäimet käyttöön",
"form.prefs.label.entry_swipe": "Ota pyyhkäisyele käyttöön mobiililaitteella",
"form.prefs.label.entry_swipe": "Ota syöttöpyyhkäisy käyttöön kosketusnäytöissä",
"form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä",
"form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
"form.prefs.label.custom_css": "Mukautettu CSS",
"form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
"form.import.label.file": "OPML-tiedosto",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Ota Fever API käyttöön",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket-käyttöoikeustunnus",
"form.integration.pocket_connect_link": "Yhdistä Pocket-tilisi",
"form.integration.wallabag_activate": "Tallenna artikkelit Wallabagiin",
"form.integration.wallabag_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
"form.integration.wallabag_endpoint": "Wallabag API -päätepiste",
"form.integration.wallabag_client_id": "Wallabag Client ID",
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Tallenna artikkelit Linkkiin",
"form.integration.linkding_endpoint": "Linkding API-päätepiste",
"form.integration.linkding_api_key": "Linkding API-avain",
"form.integration.matrix_bot_activate": "Siirrä uudet artikkelit Matrixiin",
"form.integration.matrix_bot_user": "Matrixin käyttäjätunnus",
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
"form.integration.matrix_bot_url": "Matrix-palvelimen URL-osoite",
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Ladataan...",
"form.submit.saving": "Tallennetaan...",
+18 -3
View File
@@ -28,7 +28,7 @@
"menu.integrations": "Intégrations",
"menu.sessions": "Sessions",
"menu.users": "Utilisateurs",
"menu.about": "A propos",
"menu.about": "À propos",
"menu.export": "Export",
"menu.import": "Import",
"menu.create_category": "Créer une catégorie",
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Faire défiler l'élément vers le haut",
"page.keyboard_shortcuts.remove_feed": "Supprimer ce flux",
"page.keyboard_shortcuts.go_to_search": "Mettre le focus sur le champ de recherche",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Fermer la boite de dialogue",
"page.users.title": "Utilisateurs",
"page.users.username": "Nom d'utilisateur",
@@ -263,6 +264,7 @@
"error.invalid_timezone": "Fuseau horaire non valide.",
"error.invalid_entry_direction": "Ordre de trie non valide.",
"error.invalid_display_mode": "Mode d'affichage de l'application web non valide.",
"error.invalid_gesture_nav": "Navigation gestuelle non valide.",
"error.invalid_default_home_page": "Page d'accueil par défaut invalide !",
"form.feed.label.title": "Titre",
"form.feed.label.site_url": "URL du site web",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Entrées par page",
"form.prefs.label.default_reading_speed": "Vitesse de lecture pour les autres langues (mots par minute)",
"form.prefs.label.cjk_reading_speed": "Vitesse de lecture pour le Chinois, le Coréen et le Japonais (caractères par minute)",
"form.prefs.label.display_mode": "Mode d'affichage de l'application web (doit être réinstallé)",
"form.prefs.label.display_mode": "Mode d'affichage de l'Application Web Progressive (PWA)",
"form.prefs.select.older_first": "Ancien éléments en premier",
"form.prefs.select.recent_first": "Éléments récents en premier",
"form.prefs.select.fullscreen": "Plein écran",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Navigateur",
"form.prefs.select.publish_time": "Heure de publication de l'entrée",
"form.prefs.select.created_time": "Heure de création de l'entrée",
"form.prefs.select.alphabetical": "Alphabétique",
"form.prefs.select.unread_count": "Nombre d'articles non lus",
"form.prefs.select.none": "Aucun",
"form.prefs.select.tap": "Tapez deux fois",
"form.prefs.select.swipe": "Glisser",
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",
"form.prefs.label.entry_swipe": "Activer le geste de balayage sur les entrées sur mobile",
"form.prefs.label.entry_swipe": "Activer le balayage des entrées sur les écrans tactiles",
"form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées",
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
"form.prefs.label.custom_css": "CSS personnalisé",
"form.prefs.label.entry_order": "Colonne de tri des entrées",
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
"form.import.label.file": "Fichier OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Activer l'API de Fever",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Jeton d'accès de l'API de Pocket",
"form.integration.pocket_connect_link": "Connectez votre compte Pocket",
"form.integration.wallabag_activate": "Sauvegarder les articles vers Wallabag",
"form.integration.wallabag_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
"form.integration.wallabag_endpoint": "URL de l'API de Wallabag",
"form.integration.wallabag_client_id": "Identifiant unique du client Wallabag",
"form.integration.wallabag_client_secret": "Clé secrète du client Wallabag",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Sauvegarder les articles vers Linkding",
"form.integration.linkding_endpoint": "URL de l'API de Linkding",
"form.integration.linkding_api_key": "Clé d'API de Linkding",
"form.integration.matrix_bot_activate": "Envoyer les nouveaux articles vers Matrix",
"form.integration.matrix_bot_user": "Nom de l'utilisateur Matrix",
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
"form.integration.matrix_bot_url": "URL du serveur Matrix",
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
"form.api_key.label.description": "Libellé de la clé d'API",
"form.submit.loading": "Chargement...",
"form.submit.saving": "Sauvegarde en cours...",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "आइटम को ऊपर तक स्क्रॉल करें",
"page.keyboard_shortcuts.remove_feed": "यह फ़ीड हटाएं",
"page.keyboard_shortcuts.go_to_search": "सर्च फॉर्म पर फोकस सेट करें",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "मोडल डायलॉग बंद करें",
"page.users.title": "उपभोक्ता",
"page.users.username": "यूसर्नेम",
@@ -241,6 +242,7 @@
"error.invalid_timezone": "अमान्य समयक्षेत्र.",
"error.invalid_entry_direction": "अमान्य प्रवेश दिशा।",
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
"error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।",
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
"error.empty_file": "यह फ़ाइल खाली है।",
"error.bad_credentials": "अमान्य उपयोगकर्ता नाम या पासवर्ड।",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "प्रति पृष्ठ प्रविष्टियाँ",
"form.prefs.label.default_reading_speed": "अन्य भाषाओं के लिए पढ़ने की गति (प्रति मिनट शब्द)",
"form.prefs.label.cjk_reading_speed": "चीनी, कोरियाई और जापानी के लिए पढ़ने की गति (प्रति मिनट वर्ण)",
"form.prefs.label.display_mode": "वेब ऐप डिस्प्ले मोड (पुनः स्थापित करने की आवश्यकता है)",
"form.prefs.label.display_mode": "प्रोग्रेसिव वेब ऐप (PWA) डिस्प्ले मोड",
"form.prefs.select.older_first": "पहले पुरानी प्रविष्टियाँ",
"form.prefs.select.recent_first": "हाल की प्रविष्टियाँ पहले",
"form.prefs.select.fullscreen": "पूर्ण स्क्रीन",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "ब्राउज़र",
"form.prefs.select.publish_time": "प्रवेश प्रकाशित समय",
"form.prefs.select.created_time": "प्रवेश बनाया समय",
"form.prefs.select.alphabetical": "वर्णक्रम",
"form.prefs.select.unread_count": "अपठित गणना",
"form.prefs.select.none": "कोई नहीं",
"form.prefs.select.tap": "दो बार टैप",
"form.prefs.select.swipe": "कड़ी चोट",
"form.prefs.label.keyboard_shortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
"form.prefs.label.entry_swipe": "मोबाइल पर प्रविष्टियों पर स्वाइप जेस्चर सक्षम करें",
"form.prefs.label.entry_swipe": "टच स्क्रीन पर एंट्री स्वाइप सक्षम करें",
"form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा",
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
"form.prefs.label.custom_css": "कस्टम सीएसएस",
"form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
"form.import.label.file": "ओपीएमएल फ़ाइल",
"form.import.label.url": "यूआरएल",
"form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "पॉकेट एक्सेस टोकन",
"form.integration.pocket_connect_link": "अपना पॉकेट खाता कनेक्ट करें",
"form.integration.wallabag_activate": "विषय सहेजें वालाबाग में ",
"form.integration.wallabag_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
"form.integration.wallabag_endpoint": "वालबैग एपीआई एंडपॉइंट",
"form.integration.wallabag_client_id": "वालाबैग क्लाइंट आईडी",
"form.integration.wallabag_client_secret": "वालाबैग क्लाइंट सीक्रेट",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "लिंक्डिन में विषयवस्तु सहेजें",
"form.integration.linkding_endpoint": "लिंकिंग एपीआई समापन बिंदु",
"form.integration.linkding_api_key": "लिंकिंग एपीआई कुंजी",
"form.integration.matrix_bot_activate": "नए लेखों को मैट्रिक्स में स्थानांतरित करें",
"form.integration.matrix_bot_user": "मैट्रिक्स के लिए उपयोगकर्ता नाम",
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
"form.integration.matrix_bot_url": "मैट्रिक्स सर्वर URL",
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
"form.api_key.label.description": "एपीआई कुंजी लेबल",
"form.submit.loading": "लोड हो रहा है...",
"form.submit.saving": "सहेजा जा रहा है...",
+409
View File
@@ -0,0 +1,409 @@
{
"confirm.question": "Apakah Anda yakin?",
"confirm.yes": "ya",
"confirm.no": "tidak",
"confirm.loading": "Sedang progres...",
"action.subscribe": "Langgan",
"action.save": "Simpan",
"action.or": "atau",
"action.cancel": "batal",
"action.remove": "Hapus",
"action.remove_feed": "Hapus umpan ini",
"action.update": "Perbarui",
"action.edit": "Sunting",
"action.download": "Unduh",
"action.import": "Impor",
"action.login": "Masuk",
"action.home_screen": "Tambahkan ke beranda",
"tooltip.keyboard_shortcuts": "Pintasan Papan Tik: %s",
"tooltip.logged_user": "Masuk sebagai %s",
"menu.unread": "Belum Dibaca",
"menu.starred": "Markah",
"menu.history": "Riwayat",
"menu.feeds": "Umpan",
"menu.categories": "Kategori",
"menu.settings": "Pengaturan",
"menu.logout": "Keluar",
"menu.preferences": "Preferensi",
"menu.integrations": "Integrasi",
"menu.sessions": "Sesi",
"menu.users": "Pengguna",
"menu.about": "Tentang",
"menu.export": "Ekspor",
"menu.import": "Impor",
"menu.create_category": "Buat kategori",
"menu.mark_page_as_read": "Tandai halaman ini sebagai telah dibaca",
"menu.mark_all_as_read": "Tandai semua sebagai telah dibaca",
"menu.show_all_entries": "Tampilkan semua entri",
"menu.show_only_unread_entries": "Tampilkan hanya entri yang belum dibaca",
"menu.refresh_feed": "Muat ulang",
"menu.refresh_all_feeds": "Muat ulang semua umpan di latar belakang",
"menu.edit_feed": "Sunting",
"menu.edit_category": "Sunting",
"menu.add_feed": "Tambah langganan",
"menu.add_user": "Tambah pengguna",
"menu.flush_history": "Hapus riwayat",
"menu.feed_entries": "Entri",
"menu.api_keys": "Kunci API",
"menu.create_api_key": "Buat kunci API baru",
"menu.shared_entries": "Entri yang Dibagikan",
"search.label": "Cari",
"search.placeholder": "Cari...",
"pagination.next": "Berikutnya",
"pagination.previous": "Sebelumnya",
"entry.status.unread": "Belum dibaca",
"entry.status.read": "Telah dibaca",
"entry.status.toast.unread": "Ditandai sebagai belum dibaca",
"entry.status.toast.read": "Ditandai sebagai telah dibaca",
"entry.status.title": "Ubah status entri",
"entry.bookmark.toggle.on": "Markahi",
"entry.bookmark.toggle.off": "Batal Markahi",
"entry.bookmark.toast.on": "Markahi",
"entry.bookmark.toast.off": "Batal Markahi",
"entry.state.saving": "Menyimpan...",
"entry.state.loading": "Memuat...",
"entry.save.label": "Simpan",
"entry.save.title": "Simpan artikel ini",
"entry.save.completed": "Selesai!",
"entry.save.toast.completed": "Artikel tersimpan",
"entry.scraper.label": "Unduh",
"entry.scraper.title": "Ambil konten asli",
"entry.scraper.completed": "Selesai!",
"entry.external_link.label": "Tautan eksternal",
"entry.comments.label": "Komentar",
"entry.comments.title": "Lihat Komentar",
"entry.share.label": "Bagikan",
"entry.share.title": "Bagikan artikel ini",
"entry.unshare.label": "Batal bagikan",
"entry.shared_entry.title": "Buka tautan publik",
"entry.shared_entry.label": "Bagikan",
"entry.estimated_reading_time": [
"%d menit untuk dibaca"
],
"page.shared_entries.title": "Entri yang Dibagikan",
"page.unread.title": "Belum Dibaca",
"page.starred.title": "Markah",
"page.categories.title": "Kategori",
"page.categories.no_feed": "Tidak ada umpan.",
"page.categories.entries": "Artikel",
"page.categories.feeds": "Langganan",
"page.categories.feed_count": [
"Ada %d umpan."
],
"page.categories.unread_counter": "Jumlah entri yang belum dibaca",
"page.new_category.title": "Kategori Baru",
"page.new_user.title": "Pengguna Baru",
"page.edit_category.title": "Sunting Kategori: %s",
"page.edit_user.title": "Sunting Pengguna: %s",
"page.feeds.title": "Umpan",
"page.feeds.last_check": "Terakhir diperiksa:",
"page.feeds.unread_counter": "Jumlah entri yang belum dibaca",
"page.feeds.read_counter": "Jumlah entri yang telah dibaca",
"page.feeds.error_count": [
"%d galat"
],
"page.history.title": "Riwayat",
"page.import.title": "Impor",
"page.search.title": "Hasil Pencarian",
"page.about.title": "Tentang",
"page.about.credits": "Pengembang",
"page.about.version": "Versi:",
"page.about.build_date": "Tanggal Penyusunan:",
"page.about.author": "Pengembang:",
"page.about.license": "Lisensi:",
"page.about.global_config_options": "Pengaturan Konfigurasi Global",
"page.about.postgres_version": "Versi Postgres:",
"page.about.go_version": "Versi Go:",
"page.add_feed.title": "Langganan Baru",
"page.add_feed.no_category": "Tidak ada kategori. Anda harus paling tidak memiliki satu kategori.",
"page.add_feed.label.url": "URL",
"page.add_feed.submit": "Cari langganan",
"page.add_feed.legend.advanced_options": "Pilihan Tingkat Lanjut",
"page.add_feed.choose_feed": "Pilih Umpan",
"page.edit_feed.title": "Sunting Umpan: %s",
"page.edit_feed.last_check": "Terakhir diperiksa:",
"page.edit_feed.last_modified_header": "Tajuk LastModified:",
"page.edit_feed.etag_header": "Tajuk ETag:",
"page.edit_feed.no_header": "Tidak Ada",
"page.edit_feed.last_parsing_error": "Galat Penguraian Terakhir",
"page.entry.attachments": "Lampiran",
"page.keyboard_shortcuts.title": "Pintasan Papan Tik",
"page.keyboard_shortcuts.subtitle.sections": "Navigasi Bagian",
"page.keyboard_shortcuts.subtitle.items": "Navigasi Entri",
"page.keyboard_shortcuts.subtitle.pages": "Navigasi Halaman",
"page.keyboard_shortcuts.subtitle.actions": "Tindakan",
"page.keyboard_shortcuts.go_to_unread": "Ke bagian yang belum dibaca",
"page.keyboard_shortcuts.go_to_starred": "Ke markah",
"page.keyboard_shortcuts.go_to_history": "Ke riwayat",
"page.keyboard_shortcuts.go_to_feeds": "Ke umpan",
"page.keyboard_shortcuts.go_to_categories": "Ke kategori",
"page.keyboard_shortcuts.go_to_settings": "Ke pengaturan",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Tampilkan pintasan papan tik",
"page.keyboard_shortcuts.go_to_previous_item": "Ke entri sebelumnya",
"page.keyboard_shortcuts.go_to_next_item": "Ke entri berikutnya",
"page.keyboard_shortcuts.go_to_feed": "Ke umpan",
"page.keyboard_shortcuts.go_to_previous_page": "Ke halaman sebelumnya",
"page.keyboard_shortcuts.go_to_next_page": "Ke halaman berikutnya",
"page.keyboard_shortcuts.open_item": "Buka entri yang dipilih",
"page.keyboard_shortcuts.open_original": "Buka tautan asli",
"page.keyboard_shortcuts.open_original_same_window": "Buka tautan asli di bilah saat ini",
"page.keyboard_shortcuts.open_comments": "Buka tautan komentar",
"page.keyboard_shortcuts.open_comments_same_window": "Buka tautan komentar di bilah saat ini",
"page.keyboard_shortcuts.toggle_read_status_next": "Ubah status baca, fokus ke selanjutnya",
"page.keyboard_shortcuts.toggle_read_status_prev": "Ubah status baca, fokus ke sebelumnya",
"page.keyboard_shortcuts.refresh_all_feeds": "Muat ulang semua umpan di latar belakang",
"page.keyboard_shortcuts.mark_page_as_read": "Tandai halaman saat ini sebagai telah dibaca",
"page.keyboard_shortcuts.download_content": "Unduh konten asli",
"page.keyboard_shortcuts.toggle_bookmark_status": "Ubah status markah",
"page.keyboard_shortcuts.save_article": "Simpan Artikel",
"page.keyboard_shortcuts.scroll_item_to_top": "Gulir ke atas",
"page.keyboard_shortcuts.remove_feed": "Hapus umpan ini",
"page.keyboard_shortcuts.go_to_search": "Atur fokus ke pencaarian",
"page.keyboard_shortcuts.toggle_entry_attachments": "Buka/tutup lampiran entri",
"page.keyboard_shortcuts.close_modal": "Tutup bilah modal",
"page.users.title": "Pengguna",
"page.users.username": "Nama Pengguna",
"page.users.never_logged": "Tidak Pernah",
"page.users.admin.yes": "Ya",
"page.users.admin.no": "Tidak",
"page.users.actions": "Tindakan",
"page.users.last_login": "Terakhir Masuk",
"page.users.is_admin": "Administrator",
"page.settings.title": "Pengaturan",
"page.settings.link_google_account": "Tautkan akun Google saya",
"page.settings.unlink_google_account": "Putuskan akun Google saya",
"page.settings.link_oidc_account": "Tautkan akun OpenID Connect saya",
"page.settings.unlink_oidc_account": "Putuskan akun OpenID Connect saya",
"page.login.title": "Masuk",
"page.login.google_signin": "Masuk dengan Google",
"page.login.oidc_signin": "Masuk dengan OpenID Connect",
"page.integrations.title": "Integrasi",
"page.integration.miniflux_api": "API Miniflux",
"page.integration.miniflux_api_endpoint": "Titik URL API",
"page.integration.miniflux_api_username": "Nama Pengguna",
"page.integration.miniflux_api_password": "Kata Sandi",
"page.integration.miniflux_api_password_value": "Kata sandi akun Anda",
"page.integration.bookmarklet": "Bookmarklet",
"page.integration.bookmarklet.name": "Tambahkan ke Miniflux",
"page.integration.bookmarklet.instructions": "Seret dan tempatkan tautan ini ke markah Anda.",
"page.integration.bookmarklet.help": "Tautan spesial ini memperbolehkan Anda untuk berlangganan ke situs langsung dengan menggunakan markah di peramban web Anda.",
"page.sessions.title": "Sesi",
"page.sessions.table.date": "Tanggal",
"page.sessions.table.ip": "Alamat IP",
"page.sessions.table.user_agent": "User Agent",
"page.sessions.table.actions": "Tindakan",
"page.sessions.table.current_session": "Sesi Saat Ini",
"page.api_keys.title": "Kunci API",
"page.api_keys.table.description": "Deskripsi",
"page.api_keys.table.token": "Token",
"page.api_keys.table.last_used_at": "Terakhir Digunakan",
"page.api_keys.table.created_at": "Tanggal Pembuatan",
"page.api_keys.table.actions": "Tindakan",
"page.api_keys.never_used": "Tidak Pernah Digunakan",
"page.new_api_key.title": "Kunci API Baru",
"page.offline.title": "Mode Luring",
"page.offline.message": "Anda sedang luring",
"page.offline.refresh_page": "Coba untuk memuat ulang halaman ini",
"alert.no_shared_entry": "Tidak ada entri yang dibagikan.",
"alert.no_bookmark": "Tidak ada markah.",
"alert.no_category": "Tidak ada kategori.",
"alert.no_category_entry": "Tidak ada artikel di kategori ini.",
"alert.no_feed_entry": "Tidak ada artikel di umpan ini.",
"alert.no_feed": "Anda tidak memiliki langganan.",
"alert.no_feed_in_category": "Tidak ada langganan untuk kategori ini.",
"alert.no_history": "Tidak ada riwayat untuk saat ini.",
"alert.feed_error": "Ada masalah dengan umpan ini",
"alert.no_search_result": "Tidak ada hasil untuk pencarian ini.",
"alert.no_unread_entry": "Belum ada artikel yang dibaca.",
"alert.no_user": "Anda adalah satu-satunya pengguna.",
"alert.account_unlinked": "Akun eksternal Anda sudah terputus!",
"alert.account_linked": "Akun eksternal Anda sudah terhubung!",
"alert.pocket_linked": "Akun Pocket Anda sudah terhubung!",
"alert.prefs_saved": "Preferensi disimpan!",
"error.unlink_account_without_password": "Anda harus mengatur kata sandi atau Anda tidak bisa masuk kembali.",
"error.duplicate_linked_account": "Sudah ada orang lain yang terhubung dengan penyedia ini!",
"error.duplicate_fever_username": "Sudah ada orang lain dengan nama pengguna Fever yang sama!",
"error.duplicate_googlereader_username": "Sudah ada orang lain dengan nama pengguna Google Reader yang sama!",
"error.pocket_request_token": "Tidak bisa mendapatkan token permintaan dari Pocket!",
"error.pocket_access_token": "Tidak bisa mendapatkan token akses dari Pocket!",
"error.category_already_exists": "Kategori ini telah ada.",
"error.unable_to_create_category": "Tidak bisa membuat kategori ini.",
"error.unable_to_update_category": "Tidak bisa memperbarui kategori ini.",
"error.user_already_exists": "Pengguna ini sudah ada.",
"error.unable_to_create_user": "Tidak bisa membuat pengguna tersebut.",
"error.unable_to_update_user": "Tidak bisa memperbarui pengguna tersebut.",
"error.unable_to_update_feed": "Tidak bisa memperbarui umpan ini.",
"error.subscription_not_found": "Tidak bisa mencari langganan apa pun.",
"error.invalid_theme": "Tema tidak valid.",
"error.invalid_language": "Bahasa tidak valid.",
"error.invalid_timezone": "Zona waktu tidak valid.",
"error.invalid_entry_direction": "Urutan entri tidak valid.",
"error.invalid_display_mode": "Mode tampilan aplikasi web tidak valid.",
"error.invalid_gesture_nav": "Navigasi gestur tidak valid.",
"error.invalid_default_home_page": "Beranda baku tidak valid!",
"error.empty_file": "Berkas ini kosong.",
"error.bad_credentials": "Nama pengguna atau kata sandi tidak valid.",
"error.fields_mandatory": "Semua bidang diharuskan.",
"error.title_required": "Judul diharuskan.",
"error.different_passwords": "Kata sandi tidak sama.",
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
"error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
"error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
"error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
"error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
"error.feed_already_exists": "Umpan ini sudah ada.",
"error.invalid_feed_url": "URL umpan tidak valid.",
"error.invalid_site_url": "URL situs tidak valid.",
"error.feed_url_not_empty": "URL umpan tidak boleh kosong.",
"error.site_url_not_empty": "URL situs tidak boleh kosong.",
"error.feed_title_not_empty": "Judul umpan tidak boleh kosong.",
"error.feed_category_not_found": "Kategori ini tidak ada atau tidak dipunyai oleh pengguna ini.",
"error.feed_invalid_blocklist_rule": "Aturan blokir tidak valid.",
"error.feed_invalid_keeplist_rule": "Aturan simpan tidak valid.",
"error.user_mandatory_fields": "Harus ada nama pengguna.",
"error.api_key_already_exists": "Kunci API ini sudah ada.",
"error.unable_to_create_api_key": "Tidak bisa membuat kunci API ini.",
"form.feed.label.title": "Judul",
"form.feed.label.site_url": "URL Situs",
"form.feed.label.feed_url": "URL Umpan",
"form.feed.label.category": "Kategori",
"form.feed.label.crawler": "Ambil konten asli",
"form.feed.label.feed_username": "Nama Pengguna Umpan",
"form.feed.label.feed_password": "Kata Sandi Umpan",
"form.feed.label.user_agent": "Timpa User Agent Baku",
"form.feed.label.cookie": "Atur Kuki",
"form.feed.label.scraper_rules": "Aturan Pengambil Data",
"form.feed.label.rewrite_rules": "Aturan Tulis Ulang",
"form.feed.label.blocklist_rules": "Aturan Blokir",
"form.feed.label.keeplist_rules": "Aturan Simpan",
"form.feed.label.urlrewrite_rules": "Aturan Tulis Ulang URL",
"form.feed.label.ignore_http_cache": "Abaikan Tembolok HTTP",
"form.feed.label.allow_self_signed_certificates": "Perbolehkan sertifikat web tidak valid atau sertifikasi sendiri",
"form.feed.label.fetch_via_proxy": "Ambil via Proksi",
"form.feed.label.disabled": "Jangan perbarui umpan ini",
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
"form.category.label.title": "Judul",
"form.category.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
"form.user.label.username": "Nama Pengguna",
"form.user.label.password": "Kata Sandi",
"form.user.label.confirmation": "Konfirmasi Kata Sandi",
"form.user.label.admin": "Administrator",
"form.prefs.label.language": "Bahasa",
"form.prefs.label.timezone": "Zona Waktu",
"form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Pengurutan Entri",
"form.prefs.label.entries_per_page": "Entri per Halaman",
"form.prefs.label.default_reading_speed": "Kecepatan membaca untuk bahasa lain (kata per menit)",
"form.prefs.label.cjk_reading_speed": "Kecepatan membaca untuk bahasa Tiongkok, Korea, dan Jepang (karakter per menit)",
"form.prefs.label.display_mode": "Mode Tampilan Aplikasi Web (perlu pemasangan ulang)",
"form.prefs.select.older_first": "Entri tertua dulu",
"form.prefs.select.recent_first": "Entri terbaru dulu",
"form.prefs.select.fullscreen": "Layar Penuh",
"form.prefs.select.standalone": "Tersendiri",
"form.prefs.select.minimal_ui": "Minimal",
"form.prefs.select.browser": "Peramban",
"form.prefs.select.publish_time": "Waktu entri dipublikasikan",
"form.prefs.select.created_time": "Waktu entri dibuat",
"form.prefs.select.alphabetical": "Secara alfabet",
"form.prefs.select.unread_count": "Jumlah yang belum dibaca",
"form.prefs.select.none": "Tidak ada",
"form.prefs.select.tap": "Ketuk dua kali",
"form.prefs.select.swipe": "Geser",
"form.prefs.label.keyboard_shortcuts": "Aktifkan pintasan papan tik",
"form.prefs.label.entry_swipe": "Aktifkan tindakan geser pada entri di ponsel",
"form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri",
"form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
"form.prefs.label.custom_css": "Modifikasi CSS",
"form.prefs.label.entry_order": "Pengurutan Kolom Entri",
"form.prefs.label.default_home_page": "Beranda Baku",
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
"form.import.label.file": "Berkas OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Aktifkan API Fever",
"form.integration.fever_username": "Nama Pengguna Fever",
"form.integration.fever_password": "Kata Sandi Fever",
"form.integration.fever_endpoint": "Titik URL API Fever:",
"form.integration.googlereader_activate": "Aktifkan API Google Reader",
"form.integration.googlereader_username": "Nama Pengguna Google Reader",
"form.integration.googlereader_password": "Kata Sandi Google Reader",
"form.integration.googlereader_endpoint": "Titik URL API Google Reader:",
"form.integration.pinboard_activate": "Simpan artikel ke Pinboard",
"form.integration.pinboard_token": "Token API Pinboard",
"form.integration.pinboard_tags": "Tanda di Pinboard",
"form.integration.pinboard_bookmark": "Tandai markah sebagai belum dibaca",
"form.integration.instapaper_activate": "Simpan artikel ke Instapaper",
"form.integration.instapaper_username": "Nama Pengguna Instapaper",
"form.integration.instapaper_password": "Kata Sandi Instapaper",
"form.integration.pocket_activate": "Simpan artikel ke Pocket",
"form.integration.pocket_consumer_key": "Kunci Pelanggan Pocket",
"form.integration.pocket_access_token": "Token Akses Pocket",
"form.integration.pocket_connect_link": "Hubungkan akun Pocket Anda",
"form.integration.wallabag_activate": "Simpan artikel ke Wallabag",
"form.integration.wallabag_only_url": "Kirim hanya URL (alih-alih konten penuh)",
"form.integration.wallabag_endpoint": "Titik URL API Wallabag",
"form.integration.wallabag_client_id": "ID Klien Wallabag",
"form.integration.wallabag_client_secret": "Rahasia Klien Wallabag",
"form.integration.wallabag_username": "Nama Pengguna Wallabag",
"form.integration.wallabag_password": "Kata Sandi Wallabag",
"form.integration.nunux_keeper_activate": "Simpan artikel ke Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Titik URL API Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Kunci API Nunux Keeper",
"form.integration.espial_activate": "Simpan artikel ke Espial",
"form.integration.espial_endpoint": "Titik URL API Espial",
"form.integration.espial_api_key": "Kunci API Espial",
"form.integration.espial_tags": "Tanda di Espial",
"form.integration.telegram_bot_activate": "Kirim artikel baru ke percakapan Telegram",
"form.integration.telegram_bot_token": "Token Bot",
"form.integration.telegram_chat_id": "ID Obrolan",
"form.integration.linkding_activate": "Simpan artikel ke Linkding",
"form.integration.linkding_endpoint": "Titik URL API Linkding",
"form.integration.linkding_api_key": "Kunci API Linkding",
"form.integration.matrix_bot_activate": "Kirim entri baru ke Matrix",
"form.integration.matrix_bot_user": "Nama Pengguna Matrix",
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
"form.integration.matrix_bot_url": "URL Peladen Matrix",
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
"form.api_key.label.description": "Label Kunci API",
"form.submit.loading": "Memuat...",
"form.submit.saving": "Menyimpan...",
"time_elapsed.not_yet": "belum",
"time_elapsed.yesterday": "kemarin",
"time_elapsed.now": "baru saja",
"time_elapsed.minutes": [
"%d menit yang lalu"
],
"time_elapsed.hours": [
"%d jam yang lalu"
],
"time_elapsed.days": [
"%d hari yang lalu"
],
"time_elapsed.weeks": [
"%d pekan yang lalu"
],
"time_elapsed.months": [
"%d bulan yang lalu"
],
"time_elapsed.years": [
"%d tahun yang lalu"
],
"This feed already exists (%s)": "Umpan ini sudah ada (%s)",
"Unable to fetch feed (Status Code = %d)": "Tidak bisa mengambil umpan (Kode Status = %d)",
"Unable to open this link: %v": "Tidak bisa membuka tautan ini: %v",
"Unable to analyze this page: %v": "Tidak bisa menganalisis halaman ini: %v",
"Unable to execute request: %v": "Tidak bisa mengeksekusi permintaan: %v",
"Unable to parse OPML file: %q": "Tidak bisa mengurai berkas OPML: %q",
"Unable to parse RSS feed: %q": "Tidak bisa mengurai umpan RSS: %q",
"Unable to parse Atom feed: %q": "Tidak bisa mengurai umpan Atom: %q",
"Unable to parse JSON feed: %q": "Tidak bisa mengurai umpan JSON: %q",
"Unable to parse RDF feed: %q": "Tidak bisa mengurai umpan RDF: %q",
"Unable to normalize encoding: %q": "Tidak dapat menormalisasi enkode: %q",
"This feed is empty": "Umpan ini kosong",
"This web page is empty": "Halaman web ini kosong",
"Invalid SSL certificate (original error: %q)": "Sertifikat SSL tidak valid (galat: %q)",
"This website is unreachable (original error: %q)": "Situs ini tidak dapat tersambung (galat: %q)",
"Website unreachable, the request timed out after %d seconds": "Situs tidak dapat tersambung, permintaan galat setelah %d detik",
"You are not authorized to access this resource (invalid username/password)": "Anda tidak memiliki izin yang cukup untuk mengakses umpan ini (nama pengguna/kata sandi tidak valid)",
"Unable to fetch this resource (Status Code = %d)": "Tidak bisa mengambil umpan ini (Kode Status = %d)",
"Resource not found (404), this feed doesn't exist anymore, check the feed URL": "Umpan tidak ditemukan (404), umpan ini tidak ada lagi, periksa URL umpan"
}
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Scorri l'articolo in alto",
"page.keyboard_shortcuts.remove_feed": "Rimuovi questo feed",
"page.keyboard_shortcuts.go_to_search": "Apri la casella di ricerca",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Chiudi la finestra di dialogo",
"page.users.title": "Utenti",
"page.users.username": "Nome utente",
@@ -263,6 +264,7 @@
"error.invalid_timezone": "Fuso orario non valido.",
"error.invalid_entry_direction": "Ordinamento non valido.",
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
"error.invalid_gesture_nav": "Navigazione gestuale non valida.",
"error.invalid_default_home_page": "Pagina iniziale predefinita non valida!",
"form.feed.label.title": "Titolo",
"form.feed.label.site_url": "URL del sito",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Articoli per pagina",
"form.prefs.label.default_reading_speed": "Velocità di lettura di altre lingue (parole al minuto)",
"form.prefs.label.cjk_reading_speed": "Velocità di lettura per cinese, coreano e giapponese (caratteri al minuto)",
"form.prefs.label.display_mode": "Modalità di visualizzazione web app (necessita la reinstallazione)",
"form.prefs.label.display_mode": "Modalità di visualizzazione dell'app Web progressiva (PWA).",
"form.prefs.select.older_first": "Prima i più vecchi",
"form.prefs.select.recent_first": "Prima i più recenti",
"form.prefs.select.fullscreen": "Schermo intero",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Browser",
"form.prefs.select.publish_time": "Ora di pubblicazione dell'entrata",
"form.prefs.select.created_time": "Tempo di creazione dell'entrata",
"form.prefs.select.alphabetical": "In ordine alfabetico",
"form.prefs.select.unread_count": "Conteggio dei non letti",
"form.prefs.select.none": "Nessuno",
"form.prefs.select.tap": "Tocca due volte",
"form.prefs.select.swipe": "Scorri",
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
"form.prefs.label.entry_swipe": "Abilita il gesto di scorrimento sulle voci sul cellulare",
"form.prefs.label.entry_swipe": "Abilita lo scorrimento della voce sui touch screen",
"form.prefs.label.gesture_nav": "Gesto per navigare tra le voci",
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
"form.prefs.label.custom_css": "CSS personalizzati",
"form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
"form.import.label.file": "File OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Abilita l'API di Fever",
@@ -334,6 +343,7 @@
"form.integration.pocket_connect_link": "Collega il tuo account Pocket",
"form.integration.wallabag_activate": "Salva gli articoli su Wallabag",
"form.integration.wallabag_endpoint": "Endpoint dell'API di Wallabag",
"form.integration.wallabag_only_url": "Invia solo URL (invece del contenuto completo)",
"form.integration.wallabag_client_id": "Client ID dell'account Wallabag",
"form.integration.wallabag_client_secret": "Client secret dell'account Wallabag",
"form.integration.wallabag_username": "Nome utente dell'account Wallabag",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Salva gli articoli su Linkding",
"form.integration.linkding_endpoint": "Endpoint dell'API di Linkding",
"form.integration.linkding_api_key": "API key dell'account Linkding",
"form.integration.matrix_bot_activate": "Trasferimento di nuovi articoli a Matrix",
"form.integration.matrix_bot_user": "Nome utente per Matrix",
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
"form.integration.matrix_bot_url": "URL del server Matrix",
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
"form.api_key.label.description": "Etichetta chiave API",
"form.submit.loading": "Caricamento in corso...",
"form.submit.saving": "Salvataggio in corso...",
+140 -125
View File
@@ -15,7 +15,7 @@
"action.import": "インポート",
"action.login": "ログイン",
"action.home_screen": "ホームスクリーンに追加",
"tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
"tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
"tooltip.logged_user": "%s としてログイン中",
"menu.unread": "未読",
"menu.starred": "星付き",
@@ -25,41 +25,41 @@
"menu.settings": "設定",
"menu.logout": "ログアウト",
"menu.preferences": "設定情報",
"menu.integrations": "関連付け",
"menu.integrations": "連携",
"menu.sessions": "セッション",
"menu.users": "ユーザー一覧",
"menu.about": "ソフトウア情報",
"menu.about": "ソフトウア情報",
"menu.export": "エクスポート",
"menu.import": "インポート",
"menu.create_category": "カテゴリを作成",
"menu.mark_page_as_read": "このページを既読にする",
"menu.mark_all_as_read": "て既読にする",
"menu.show_all_entries": "ての記事を表示",
"menu.mark_all_as_read": "すべて既読にする",
"menu.show_all_entries": "すべての記事を表示",
"menu.show_only_unread_entries": "未読の記事だけを表示",
"menu.refresh_feed": "更新",
"menu.refresh_all_feeds": "てのフィードをバックグラウンドで更新",
"menu.refresh_all_feeds": "すべてのフィードをバックグラウンドで更新",
"menu.edit_feed": "編集",
"menu.edit_category": "編集",
"menu.add_feed": "フィードを購読する",
"menu.add_feed": "フィードを購読",
"menu.add_user": "ユーザーを追加",
"menu.flush_history": "履歴を更新",
"menu.flush_history": "履歴をクリア",
"menu.feed_entries": "記事一覧",
"menu.api_keys": "APIキー",
"menu.create_api_key": "新しいAPIキーを作成する",
"menu.api_keys": "API キー",
"menu.create_api_key": "新しい API キーを作成する",
"menu.shared_entries": "共有エントリ",
"search.label": "検索",
"search.placeholder": "…を検索",
"pagination.next": "次",
"pagination.previous": "前",
"entry.status.unread": "未読",
"entry.status.read": "既読",
"entry.status.toast.unread": "未読にする",
"entry.status.toast.read": "既読にする",
"entry.status.unread": "未読にする",
"entry.status.read": "既読にする",
"entry.status.toast.unread": "未読にしました",
"entry.status.toast.read": "既読にしました",
"entry.status.title": "記事の状態を変更",
"entry.bookmark.toggle.on": "星を付ける",
"entry.bookmark.toggle.off": "星を外す",
"entry.bookmark.toast.on": "星付き",
"entry.bookmark.toast.off": "星無し",
"entry.bookmark.toast.on": "星を付けました",
"entry.bookmark.toast.off": "星を外しました",
"entry.state.saving": "保存中…",
"entry.state.loading": "読み込み中…",
"entry.save.label": "保存",
@@ -74,28 +74,28 @@
"entry.comments.title": "コメントを見る",
"entry.share.label": "共有",
"entry.share.title": "この記事を共有する",
"entry.unshare.label": "共有解除",
"entry.unshare.label": "共有解除",
"entry.shared_entry.title": "公開リンクを開く",
"entry.shared_entry.label": "共有する",
"entry.estimated_reading_time": [
"%d分で読",
"%d分で読"
"%d 分で読めます",
"%d 分で読めます"
],
"page.shared_entries.title": "共有エントリ",
"page.unread.title": "未読",
"page.starred.title": "星付き",
"page.categories.title": "カテゴリ",
"page.categories.no_feed": "フィード無し",
"page.categories.entries": "記事",
"page.categories.feeds": "フィード購読を見る",
"page.categories.no_feed": "フィードはありません。",
"page.categories.entries": "記事一覧",
"page.categories.feeds": "フィード一覧",
"page.categories.feed_count": [
"%d 個の記事があります。",
"%d 個の記事があります。"
"%d 件のフィードがあります。",
"%d 件のフィードがあります。"
],
"page.categories.unread_counter": "未読記事の数",
"page.new_category.title": "新規カテゴリ",
"page.new_user.title": "新規ユーザー",
"page.edit_category.title": "カテゴリを編集: %s",
"page.edit_category.title": "カテゴリを編集: %s",
"page.edit_user.title": "ユーザーを編集: %s",
"page.feeds.title": "フィード一覧",
"page.feeds.last_check": "最終チェック:",
@@ -108,7 +108,7 @@
"page.history.title": "履歴",
"page.import.title": "インポート",
"page.search.title": "検索結果",
"page.about.title": "ソフトウア情報",
"page.about.title": "ソフトウア情報",
"page.about.credits": "著作権表示",
"page.about.version": "バージョン:",
"page.about.build_date": "ビルド日時:",
@@ -117,51 +117,52 @@
"page.about.global_config_options": "グローバル構成オプション",
"page.about.postgres_version": "Postgres バージョン:",
"page.about.go_version": "Go バージョン:",
"page.add_feed.title": "新規購読",
"page.add_feed.no_category": "カテゴリが存在しません。 少なくとも1つのカテゴリが必要です。",
"page.add_feed.title": "新規フィード",
"page.add_feed.no_category": "カテゴリが存在しません。カテゴリが少なくとも1つ必要です。",
"page.add_feed.label.url": "URL",
"page.add_feed.submit": "購読フィードを探して追加",
"page.add_feed.legend.advanced_options": "追加の設定",
"page.add_feed.choose_feed": "購読を選択",
"page.edit_feed.title": "フィード(%s)を編集",
"page.add_feed.submit": "フィードを探して追加",
"page.add_feed.legend.advanced_options": "高度な設定",
"page.add_feed.choose_feed": "フィードを選択",
"page.edit_feed.title": "フィードを編集: %s",
"page.edit_feed.last_check": "最終チェック:",
"page.edit_feed.last_modified_header": "最後に更新されたヘッダー:",
"page.edit_feed.last_modified_header": "Last-Modified ヘッダー:",
"page.edit_feed.etag_header": "ETag ヘッダー:",
"page.edit_feed.no_header": " なし",
"page.edit_feed.last_parsing_error": "最新の解析エラー",
"page.entry.attachments": "添付",
"page.keyboard_shortcuts.title": "キーボードショートカット",
"page.keyboard_shortcuts.subtitle.sections": "セクション 移動",
"page.keyboard_shortcuts.subtitle.items": "アイテム 移動",
"page.keyboard_shortcuts.subtitle.pages": "ページ 移動",
"page.edit_feed.no_header": "なし",
"page.edit_feed.last_parsing_error": "直近の解析エラー",
"page.entry.attachments": "添付ファイル",
"page.keyboard_shortcuts.title": "キーボードショートカット",
"page.keyboard_shortcuts.subtitle.sections": "セクション移動する",
"page.keyboard_shortcuts.subtitle.items": "アイテム間を移動する",
"page.keyboard_shortcuts.subtitle.pages": "ページ間を移動する",
"page.keyboard_shortcuts.subtitle.actions": "アクション",
"page.keyboard_shortcuts.go_to_unread": "未読へ移動",
"page.keyboard_shortcuts.go_to_starred": "ブックマークへ移動",
"page.keyboard_shortcuts.go_to_history": "履歴へ移動",
"page.keyboard_shortcuts.go_to_feeds": "購読へ移動",
"page.keyboard_shortcuts.go_to_categories": "カテゴリへ移動",
"page.keyboard_shortcuts.go_to_settings": "設定に移動",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "キーボードショートカット表示",
"page.keyboard_shortcuts.go_to_unread": "未読",
"page.keyboard_shortcuts.go_to_starred": "星付き",
"page.keyboard_shortcuts.go_to_history": "履歴",
"page.keyboard_shortcuts.go_to_feeds": "フィード一覧",
"page.keyboard_shortcuts.go_to_categories": "カテゴリ",
"page.keyboard_shortcuts.go_to_settings": "設定",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "キーボードショートカット表示",
"page.keyboard_shortcuts.go_to_previous_item": "前のアイテム",
"page.keyboard_shortcuts.go_to_next_item": "次のアイテム",
"page.keyboard_shortcuts.go_to_feed": "フィードへ移動",
"page.keyboard_shortcuts.go_to_previous_page": "前のページに移動",
"page.keyboard_shortcuts.go_to_next_page": "次のページに移動",
"page.keyboard_shortcuts.go_to_feed": "フィード",
"page.keyboard_shortcuts.go_to_previous_page": "前のページ",
"page.keyboard_shortcuts.go_to_next_page": "次のページ",
"page.keyboard_shortcuts.open_item": "選択されたアイテムを開く",
"page.keyboard_shortcuts.open_original": "オリジナルのリンクを開く",
"page.keyboard_shortcuts.open_original_same_window": "現在のタブでオリジナルのリンクを開く",
"page.keyboard_shortcuts.open_comments": "コメントリンクを開く",
"page.keyboard_shortcuts.open_comments_same_window": "現在のタブでコメントリンクを開く",
"page.keyboard_shortcuts.toggle_read_status_next": "既読/未読 切り替え, 次に焦点を合わせる",
"page.keyboard_shortcuts.toggle_read_status_prev": "既読/未読 切り替え, 前にフォーカス",
"page.keyboard_shortcuts.refresh_all_feeds": "てのフィードをバックグラウンドで更新",
"page.keyboard_shortcuts.mark_page_as_read": "現在のページ既読にする",
"page.keyboard_shortcuts.toggle_read_status_next": "既読/未読切り替えて次のアイテムに移動",
"page.keyboard_shortcuts.toggle_read_status_prev": "既読/未読切り替えて前のアイテムに移動",
"page.keyboard_shortcuts.refresh_all_feeds": "すべてのフィードをバックグラウンドで更新",
"page.keyboard_shortcuts.mark_page_as_read": "現在のページの記事をすべて既読にする",
"page.keyboard_shortcuts.download_content": "オリジナルの内容をダウンロード",
"page.keyboard_shortcuts.toggle_bookmark_status": "星を付ける/外す",
"page.keyboard_shortcuts.save_article": "記事を保存",
"page.keyboard_shortcuts.scroll_item_to_top": "アイテムを上にスクロール",
"page.keyboard_shortcuts.scroll_item_to_top": "アイテムが上端になるようにスクロール",
"page.keyboard_shortcuts.remove_feed": "このフィードを削除",
"page.keyboard_shortcuts.go_to_search": "検索フォームにフォーカスを移す",
"page.keyboard_shortcuts.go_to_search": "検索フォームに移動",
"page.keyboard_shortcuts.toggle_entry_attachments": "添付ファイルを開く/閉じる",
"page.keyboard_shortcuts.close_modal": "モーダルダイアログを閉じる",
"page.users.title": "ユーザー一覧",
"page.users.username": "ユーザー名",
@@ -179,7 +180,7 @@
"page.login.title": "ログイン",
"page.login.google_signin": "Google アカウントでログイン",
"page.login.oidc_signin": "OpenID Connect アカウントでログイン",
"page.integrations.title": "関連付け",
"page.integrations.title": "連携",
"page.integration.miniflux_api": "Miniflux API",
"page.integration.miniflux_api_endpoint": "API Endpoint",
"page.integration.miniflux_api_username": "ユーザー名",
@@ -195,14 +196,14 @@
"page.sessions.table.user_agent": "User Agent",
"page.sessions.table.actions": "アクション",
"page.sessions.table.current_session": "現在のセッション",
"page.api_keys.title": "APIキー",
"page.api_keys.title": "API キー",
"page.api_keys.table.description": "説明",
"page.api_keys.table.token": "トークン",
"page.api_keys.table.last_used_at": "最終使用",
"page.api_keys.table.created_at": "作成日",
"page.api_keys.table.actions": "アクション",
"page.api_keys.never_used": "使われたことがない",
"page.new_api_key.title": "新しいAPIキー",
"page.api_keys.never_used": "未使用",
"page.new_api_key.title": "新しい API キー",
"page.offline.title": "オフラインモード",
"page.offline.message": "オフラインです",
"page.offline.refresh_page": "ページを更新してみてください",
@@ -212,8 +213,8 @@
"alert.no_category_entry": "このカテゴリには記事がありません。",
"alert.no_feed_entry": "このフィードには記事がありません。",
"alert.no_feed": "何も購読していません。",
"alert.no_feed_in_category": "このカテゴリにはフィードの購読がありません。",
"alert.no_history": "現時点では履歴ありません。",
"alert.no_feed_in_category": "このカテゴリには購読中のフィードがありません。",
"alert.no_history": "現履歴ありません。",
"alert.feed_error": "このフィードには問題があります。",
"alert.no_search_result": "検索で何も見つかりませんでした。",
"alert.no_unread_entry": "未読の記事はありません。",
@@ -228,42 +229,43 @@
"error.duplicate_googlereader_username": "既に同じ名前の Google Reader ユーザー名が使われています!",
"error.pocket_request_token": "Pocket の request token が取得できません!",
"error.pocket_access_token": "Pocket の access token が取得できません!",
"error.category_already_exists": "このカテゴリは既に存在しています。",
"error.unable_to_create_category": "カテゴリ作成できません。",
"error.unable_to_update_category": "カテゴリ更新できません。",
"error.category_already_exists": "このカテゴリは既に存在します。",
"error.unable_to_create_category": "このカテゴリ作成できません。",
"error.unable_to_update_category": "このカテゴリ更新できません。",
"error.user_already_exists": "このユーザーは既に存在します。",
"error.unable_to_create_user": "このユーザーを作ることはできません。",
"error.unable_to_update_user": "このユーザー更新することはできません。",
"error.unable_to_update_feed": "このフィード更新することはできません。",
"error.subscription_not_found": "購読フィードが見つかりません。",
"error.unable_to_create_user": "このユーザーは作成できません。",
"error.unable_to_update_user": "このユーザー更新できません。",
"error.unable_to_update_feed": "このフィード更新できません。",
"error.subscription_not_found": "フィードが見つかりません。",
"error.invalid_theme": "テーマが無効です。",
"error.invalid_language": "言語が無効です。",
"error.invalid_timezone": "タイムゾーンが無効です。",
"error.invalid_entry_direction": "記事の表示順が無効です。",
"error.invalid_display_mode": "Web アプリの表示モードが無効です。",
"error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。",
"error.invalid_default_home_page": "デフォルトのトップページが無効です",
"error.empty_file": "このファイルは空です。",
"error.bad_credentials": "ユーザー名かパスワードが間違っています。",
"error.fields_mandatory": "ての項目が必要です。",
"error.fields_mandatory": "すべての項目が必要です。",
"error.title_required": "タイトルが必要です。",
"error.different_passwords": "パスワードが一致しません。",
"error.password_min_length": "パスワードは6文字以上である必要があります。",
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのてが必要です。",
"error.settings_reading_speed_is_positive": "読み取り速度は正の整数でなければならない。",
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
"error.settings_reading_speed_is_positive": "読速度は正の整数である必要があります。",
"error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
"error.feed_already_exists": "このフィードはすでに存在します。",
"error.invalid_feed_url": "無効なフィードURL。",
"error.invalid_site_url": "無効なサイトURL。",
"error.feed_url_not_empty": "フィードURLを空にすることはできません。",
"error.site_url_not_empty": "サイトのURLを空にすることはできません。",
"error.feed_already_exists": "このフィードはに存在します。",
"error.invalid_feed_url": "フィード URL が無効です。",
"error.invalid_site_url": "サイト URL が無効です。",
"error.feed_url_not_empty": "フィード URL を空にすることはできません。",
"error.site_url_not_empty": "サイトの URL を空にすることはできません。",
"error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
"error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
"error.feed_invalid_blocklist_rule": "ブロックリストルールが無効です。",
"error.feed_invalid_keeplist_rule": "リストの保持ルールが無効です。",
"error.user_mandatory_fields": "ユーザー名が必要です。",
"error.api_key_already_exists": "このAPIキーは既に存在します。",
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
"error.invalid_theme": "テーマが無効です。",
"error.invalid_language": "言語が無効です。",
"error.invalid_timezone": "タイムゾーンが無効です。",
"error.invalid_entry_direction": "ソート順が無効です。",
"error.invalid_display_mode": "Webアプリの表示モードが無効です。",
"error.invalid_default_home_page": "デフォルトのホームページが無効です",
"error.api_key_already_exists": "この API キーは既に存在します。",
"error.unable_to_create_api_key": "この API キーを作成できません。",
"form.feed.label.title": "タイトル",
"form.feed.label.site_url": "サイト URL",
"form.feed.label.feed_url": "フィード URL",
@@ -271,20 +273,20 @@
"form.feed.label.crawler": "オリジナルの内容を取得",
"form.feed.label.feed_username": "フィードのユーザー名",
"form.feed.label.feed_password": "フィードのパスワード",
"form.feed.label.user_agent": "デフォルトの User Agent を上書きする",
"form.feed.label.cookie": "クッキーの設定",
"form.feed.label.scraper_rules": "スクラップルール",
"form.feed.label.user_agent": "デフォルトの User Agent を上書きする",
"form.feed.label.cookie": "Cookie の設定",
"form.feed.label.scraper_rules": "Scraper ルール",
"form.feed.label.rewrite_rules": "Rewrite ルール",
"form.feed.label.blocklist_rules": "ブロックルール",
"form.feed.label.keeplist_rules": "許可規則",
"form.feed.label.urlrewrite_rules": "URL書き換えルール",
"form.feed.label.blocklist_rules": "Block ルール",
"form.feed.label.keeplist_rules": "Keep ルール",
"form.feed.label.urlrewrite_rules": "Rewrite URL ルール",
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
"form.feed.label.allow_self_signed_certificates": "自己署名証明書または無効な証明書を許可する",
"form.feed.label.fetch_via_proxy": "プロキシ経由でフェッチ",
"form.feed.label.fetch_via_proxy": "プロキシ経由で取得",
"form.feed.label.disabled": "このフィードを更新しない",
"form.feed.label.hide_globally": "グローバル未読リストのエントリーを隠す",
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
"form.category.label.title": "タイトル",
"form.category.hide_globally": "グローバル未読リストのエントリーを隠す",
"form.category.hide_globally": "未読一覧に記事を表示しない",
"form.user.label.username": "ユーザー名",
"form.user.label.password": "パスワード",
"form.user.label.confirmation": "パスワード確認",
@@ -292,52 +294,60 @@
"form.prefs.label.language": "言語",
"form.prefs.label.timezone": "タイムゾーン",
"form.prefs.label.theme": "テーマ",
"form.prefs.label.entry_sorting": "記事の並べ替え",
"form.prefs.label.entries_per_page": "ページあたりのエントリ",
"form.prefs.label.default_reading_speed": "他言語の読速度(単語/分)",
"form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(1分間あたりの文字数)",
"form.prefs.label.display_mode": "Webアプリの表示モード (再インストールが必要)",
"form.prefs.label.entry_sorting": "記事の表示順",
"form.prefs.label.entries_per_page": "ページあたりの記事数",
"form.prefs.label.default_reading_speed": "他言語の読速度(単語/分)",
"form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(文字数/分",
"form.prefs.label.display_mode": "プログレッシブ Web アプリ (PWA) 表示モード",
"form.prefs.select.older_first": "古い記事を最初に",
"form.prefs.select.recent_first": "新しい記事を最初に",
"form.prefs.select.fullscreen": "全画面表示",
"form.prefs.select.standalone": "スタンドアロン",
"form.prefs.select.minimal_ui": "最小限",
"form.prefs.select.browser": "ブラウザ",
"form.prefs.select.publish_time": "エントリー公開時",
"form.prefs.select.created_time": "エントリー作成時間",
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
"form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする",
"form.prefs.select.fullscreen": "Fullscreen",
"form.prefs.select.standalone": "Standalone",
"form.prefs.select.minimal_ui": "Minimal",
"form.prefs.select.browser": "Browser",
"form.prefs.select.publish_time": "記事の公開時",
"form.prefs.select.created_time": "記事の取得時刻",
"form.prefs.select.alphabetical": "アルファベット順",
"form.prefs.select.unread_count": "未読数",
"form.prefs.select.none": "なし",
"form.prefs.select.tap": "ダブルタップ",
"form.prefs.select.swipe": "スワイプ",
"form.prefs.label.keyboard_shortcuts": "キーボードショートカットを有効にする",
"form.prefs.label.entry_swipe": "タッチスクリーンでスワイプ入力を有効にする",
"form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
"form.prefs.label.custom_css": "カスタムCSS",
"form.prefs.label.entry_order": "エントリーソートカラム",
"form.prefs.label.custom_css": "カスタム CSS",
"form.prefs.label.entry_order": "記事の表示順の基準",
"form.prefs.label.default_home_page": "デフォルトのトップページ",
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
"form.import.label.file": "OPML ファイル",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Fever API を有効にする",
"form.integration.fever_username": "Fever の ユーザー名",
"form.integration.fever_password": "Fever の パスワード",
"form.integration.fever_username": "Fever のユーザー名",
"form.integration.fever_password": "Fever のパスワード",
"form.integration.fever_endpoint": "Fever API endpoint:",
"form.integration.googlereader_activate": "Google Reader API を有効にする",
"form.integration.googlereader_username": "Google Reader の ユーザー名",
"form.integration.googlereader_password": "Google Reader の パスワード",
"form.integration.googlereader_username": "Google Reader のユーザー名",
"form.integration.googlereader_password": "Google Reader のパスワード",
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
"form.integration.pinboard_activate": "Pinboard に記事を保存する",
"form.integration.pinboard_token": "Pinboard の API Token",
"form.integration.pinboard_tags": "Pinboard の Tag",
"form.integration.pinboard_bookmark": "ブックマークを未読にする",
"form.integration.instapaper_activate": "Instapaper に記事を保存する",
"form.integration.instapaper_username": "Instapaper の ユーザー名",
"form.integration.instapaper_password": "Instapaper の パスワード",
"form.integration.instapaper_username": "Instapaper のユーザー名",
"form.integration.instapaper_password": "Instapaper のパスワード",
"form.integration.pocket_activate": "Pocket に記事を保存する",
"form.integration.pocket_consumer_key": "Pocket の Consumer Key",
"form.integration.pocket_access_token": "Pocket の Access Token",
"form.integration.pocket_connect_link": "Pocket account に接続",
"form.integration.wallabag_activate": "Wallabag に記事を保存する",
"form.integration.wallabag_only_url": "URL のみを送信 (完全なコンテンツではなく)",
"form.integration.wallabag_endpoint": "Wallabag の API Endpoint",
"form.integration.wallabag_client_id": "Wallabag の Client ID",
"form.integration.wallabag_client_secret": "Wallabag の Client Secret",
"form.integration.wallabag_username": "Wallabag の ユーザー名",
"form.integration.wallabag_password": "Wallabag の パスワード",
"form.integration.wallabag_username": "Wallabag のユーザー名",
"form.integration.wallabag_password": "Wallabag のパスワード",
"form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint",
"form.integration.nunux_keeper_api_key": "Nunux Keeper の API key",
@@ -345,13 +355,18 @@
"form.integration.espial_endpoint": "Espial の API Endpoint",
"form.integration.espial_api_key": "Espial の API key",
"form.integration.espial_tags": "Espial の Tag",
"form.integration.telegram_bot_activate": "新しい記事をTelegramチャットにプッシュする",
"form.integration.telegram_bot_activate": "新しい記事を Telegram チャットにプッシュする",
"form.integration.telegram_bot_token": "ボットトークン",
"form.integration.telegram_chat_id": "チャットID",
"form.integration.telegram_chat_id": "チャット ID",
"form.integration.linkding_activate": "Linkding に記事を保存する",
"form.integration.linkding_endpoint": "Linkding の API Endpoint",
"form.integration.linkding_api_key": "Linkding の API key",
"form.api_key.label.description": "APIキーラベル",
"form.integration.matrix_bot_activate": "新しい記事をMatrixに転送する",
"form.integration.matrix_bot_user": "Matrixのユーザー名",
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
"form.integration.matrix_bot_url": "MatrixサーバーのURL",
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
"form.api_key.label.description": "API キーラベル",
"form.submit.loading": "読み込み中…",
"form.submit.saving": "保存中…",
"time_elapsed.not_yet": "未来",
@@ -370,12 +385,12 @@
"%d 日前"
],
"time_elapsed.weeks": [
"%d 週前",
"%d 週前"
"%d 週前",
"%d 週前"
],
"time_elapsed.months": [
"%d 月前",
"%d 月前"
"%d 月前",
"%d 月前"
],
"time_elapsed.years": [
"%d 年前",
+19 -4
View File
@@ -78,8 +78,8 @@
"entry.shared_entry.title": "Open de openbare link",
"entry.shared_entry.label": "Delen",
"entry.estimated_reading_time": [
"%d minuut gelezen",
"%d minuten gelezen"
"%d minuut leestijd",
"%d minuten leestijd"
],
"page.shared_entries.title": "Gedeelde vermeldingen",
"page.unread.title": "Ongelezen",
@@ -163,6 +163,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Sluit dialoogscherm",
"page.users.title": "Gebruikers",
"page.users.username": "Gebruikersnaam",
@@ -263,6 +264,7 @@
"error.invalid_timezone": "Ongeldige tijdzone.",
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
"error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.",
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
"error.invalid_default_home_page": "Ongeldige standaard homepage!",
"form.feed.label.title": "Naam",
"form.feed.label.site_url": "Website URL",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
"form.prefs.label.default_reading_speed": "Leessnelheid voor andere talen (woorden per minuut)",
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
"form.prefs.label.display_mode": "Weergavemodus voor webapp (moet opnieuw worden geïnstalleerd)",
"form.prefs.label.display_mode": "Weergavemodus Progressive Web App (PWA).",
"form.prefs.select.older_first": "Oudere items eerst",
"form.prefs.select.recent_first": "Recente items eerst",
"form.prefs.select.fullscreen": "Volledig scherm",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Browser",
"form.prefs.select.publish_time": "Tijd van binnenkomst",
"form.prefs.select.created_time": "Tijdstip van binnenkomst",
"form.prefs.select.alphabetical": "Alfabetisch",
"form.prefs.select.unread_count": "Ongelezen tellen",
"form.prefs.select.none": "Geen",
"form.prefs.select.tap": "Dubbeltik",
"form.prefs.select.swipe": "Vegen",
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
"form.prefs.label.entry_swipe": "Schakel veegbewegingen in voor items op mobiel",
"form.prefs.label.entry_swipe": "Invoervegen inschakelen op aanraakschermen",
"form.prefs.label.gesture_nav": "Gebaar om tussen ingangen te navigeren",
"form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
"form.prefs.label.custom_css": "Aangepaste CSS",
"form.prefs.label.entry_order": "Ingang Sorteerkolom",
"form.prefs.label.default_home_page": "Standaard startpagina",
"form.prefs.label.categories_sorting_order": "Categorieën sorteren",
"form.import.label.file": "OPML-bestand",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Activeer Fever API",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Verbind je Pocket-account",
"form.integration.wallabag_activate": "Opslaan naar Wallabag",
"form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
"form.integration.wallabag_endpoint": "Wallabag URL",
"form.integration.wallabag_client_id": "Wallabag Client-ID",
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Opslaan naar Linkding",
"form.integration.linkding_endpoint": "Linkding URL",
"form.integration.linkding_api_key": "Linkding API-sleutel",
"form.integration.matrix_bot_activate": "Nieuwe artikelen overbrengen naar Matrix",
"form.integration.matrix_bot_user": "Gebruikersnaam voor Matrix",
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
"form.integration.matrix_bot_url": "URL van de Matrix-server",
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
"form.api_key.label.description": "API-sleutellabel",
"form.submit.loading": "Laden...",
"form.submit.saving": "Opslaag...",
+18 -3
View File
@@ -164,6 +164,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Przewiń artykuł do góry",
"page.keyboard_shortcuts.remove_feed": "Usuń ten kanał",
"page.keyboard_shortcuts.go_to_search": "Ustaw fokus na formularzu wyszukiwania",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Zamknij listę skrótów klawiszowych",
"page.users.title": "Użytkownicy",
"page.users.username": "Nazwa użytkownika",
@@ -265,6 +266,7 @@
"error.invalid_timezone": "Nieprawidłowa strefa czasowa.",
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji internetowej.",
"error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.",
"error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
"form.feed.label.title": "Tytuł",
"form.feed.label.site_url": "URL strony",
@@ -283,7 +285,7 @@
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
"form.feed.label.allow_self_signed_certificates": "Zezwalaj na certyfikaty z podpisem własnym lub nieprawidłowe certyfikaty",
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
"form.feed.label.disabled": "Не обновлять этот канал",
"form.feed.label.disabled": "Nie odświeżaj tego kanału",
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
"form.category.label.title": "Tytuł",
"form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
@@ -296,12 +298,13 @@
"form.prefs.label.theme": "Wygląd",
"form.prefs.label.entry_sorting": "Sortowanie artykułów",
"form.prefs.label.entries_per_page": "Wpisy na stronie",
"form.prefs.label.default_reading_speed": "Prędkość czytania dla innych języków (słowa na minutę)",
"form.prefs.label.default_reading_speed": "Tryb wyświetlania Progressive Web App (PWA).",
"form.prefs.label.cjk_reading_speed": "Prędkość czytania dla języka chińskiego, koreańskiego i japońskiego (znaki na minutę)",
"form.prefs.label.display_mode": "Tryb wyświetlania aplikacji internetowej (wymaga ponownej instalacji)",
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
"form.prefs.label.entry_swipe": "Włącz gest przesuwania na wpisach na telefonie komórkowym",
"form.prefs.label.entry_swipe": "Włącz machnięcie wpisu na ekranach dotykowych",
"form.prefs.label.gesture_nav": "Gest, aby poruszać się między wpisami",
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów",
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
"form.prefs.select.fullscreen": "Pełny ekran",
@@ -310,9 +313,15 @@
"form.prefs.select.browser": "Przeglądarka",
"form.prefs.select.publish_time": "Czas publikacji wpisu",
"form.prefs.select.created_time": "Czas utworzenia wpisu",
"form.prefs.select.alphabetical": "Alfabetycznie",
"form.prefs.select.unread_count": "Liczba nieprzeczytanych",
"form.prefs.select.none": "Nic",
"form.prefs.select.tap": "Podwójne wciśnięcie",
"form.prefs.select.swipe": "Trzepnąć",
"form.prefs.label.custom_css": "Niestandardowy CSS",
"form.prefs.label.entry_order": "Kolumna sortowania wpisów",
"form.prefs.label.default_home_page": "Domyślna strona główna",
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
"form.import.label.file": "Plik OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Aktywuj Fever API",
@@ -335,6 +344,7 @@
"form.integration.pocket_access_token": "Token dostępu kieszeń",
"form.integration.pocket_connect_link": "Połącz swoje konto Pocket",
"form.integration.wallabag_activate": "Zapisz artykuły do Wallabag",
"form.integration.wallabag_only_url": "Wyślij tylko adres URL (zamiast pełnej treści)",
"form.integration.wallabag_endpoint": "Wallabag URL",
"form.integration.wallabag_client_id": "Wallabag Client-ID",
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
@@ -353,6 +363,11 @@
"form.integration.linkding_activate": "Zapisz artykuły do Linkding",
"form.integration.linkding_endpoint": "Linkding URL",
"form.integration.linkding_api_key": "Linkding API key",
"form.integration.matrix_bot_activate": "Przenieś nowe artykuły do Matrix",
"form.integration.matrix_bot_user": "Nazwa użytkownika dla Matrix",
"form.integration.matrix_bot_password": "Hasło dla użytkownika Matrix",
"form.integration.matrix_bot_url": "URL serwera Matrix",
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
"form.api_key.label.description": "Etykieta klucza API",
"form.submit.loading": "Ładowanie...",
"form.submit.saving": "Zapisywanie...",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Role o item para cima",
"page.keyboard_shortcuts.remove_feed": "Remover essa fonte",
"page.keyboard_shortcuts.go_to_search": "Ir para o campo de busca",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Fechar janela",
"page.users.title": "Usuários",
"page.users.username": "Nome de usuário",
@@ -263,6 +264,7 @@
"error.invalid_timezone": "Fuso horário inválido.",
"error.invalid_entry_direction": "Direção de entrada inválida.",
"error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.",
"error.invalid_gesture_nav": "Navegação por gestos inválida.",
"error.invalid_default_home_page": "Página inicial por defeito inválida!",
"form.feed.label.title": "Título",
"form.feed.label.site_url": "URL do site",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Itens por página",
"form.prefs.label.default_reading_speed": "Velocidade de leitura para outros idiomas (palavras por minuto)",
"form.prefs.label.cjk_reading_speed": "Velocidade de leitura para chinês, coreano e japonês (caracteres por minuto)",
"form.prefs.label.display_mode": "Modo de exibição do aplicativo Web (precisa ser reinstalado)",
"form.prefs.label.display_mode": "Modo de exibição Progressive Web App (PWA)",
"form.prefs.select.older_first": "Itens mais velhos primeiro",
"form.prefs.select.recent_first": "Itens mais recentes",
"form.prefs.select.fullscreen": "Tela completa",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Navegador",
"form.prefs.select.publish_time": "Entrada hora de publicação",
"form.prefs.select.created_time": "Entrada tempo criado",
"form.prefs.select.alphabetical": "Por ordem alfabética",
"form.prefs.select.unread_count": "Contagem não lida",
"form.prefs.select.none": "Nenhum",
"form.prefs.select.tap": "Toque duplo",
"form.prefs.select.swipe": "Deslize",
"form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado",
"form.prefs.label.entry_swipe": "Ativar gesto de deslizar nas entradas no celular",
"form.prefs.label.entry_swipe": "Ativar entrada de furto em telas sensíveis ao toque",
"form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas",
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
"form.prefs.label.custom_css": "CSS customizado",
"form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
"form.prefs.label.default_home_page": "Página inicial predefinida",
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
"form.import.label.file": "Arquivo OPML",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Ativar API do Fever",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Token de acesso do Pocket",
"form.integration.pocket_connect_link": "Conectar a conta do Pocket",
"form.integration.wallabag_activate": "Salvar itens no Wallabag",
"form.integration.wallabag_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
"form.integration.wallabag_endpoint": "Endpoint da API do Wallabag",
"form.integration.wallabag_client_id": "ID de cliente (Client ID) do Wallabag",
"form.integration.wallabag_client_secret": "Segredo do cliente (Client Secret) do Wallabag",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Salvar itens no Linkding",
"form.integration.linkding_endpoint": "Endpoint de API do Linkding",
"form.integration.linkding_api_key": "Chave de API do Linkding",
"form.integration.matrix_bot_activate": "Transferir novos artigos para o Matrix",
"form.integration.matrix_bot_user": "Nome de utilizador para Matrix",
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
"form.integration.matrix_bot_url": "URL do servidor Matrix",
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
"form.api_key.label.description": "Etiqueta da chave de API",
"form.submit.loading": "Carregando...",
"form.submit.saving": "Salvando...",
+17 -2
View File
@@ -164,6 +164,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Прокрутите элемент вверх",
"page.keyboard_shortcuts.remove_feed": "Удалить эту подписку",
"page.keyboard_shortcuts.go_to_search": "Установить фокус в поисковой форме",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Закрыть модальный диалог",
"page.users.title": "Пользователи",
"page.users.username": "Имя пользователя",
@@ -265,6 +266,7 @@
"error.invalid_timezone": "Неверный часовой пояс.",
"error.invalid_entry_direction": "Неверное направление входа.",
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
"error.invalid_gesture_nav": "Неверная жестовая навигация.",
"error.invalid_default_home_page": "Неверная домашняя страница по умолчанию!",
"form.feed.label.title": "Название",
"form.feed.label.site_url": "URL сайта",
@@ -298,7 +300,7 @@
"form.prefs.label.entries_per_page": "Записи на странице",
"form.prefs.label.default_reading_speed": "Скорость чтения на других языках (слов в минуту)",
"form.prefs.label.cjk_reading_speed": "Скорость чтения на китайском, корейском и японском языках (знаков в минуту)",
"form.prefs.label.display_mode": "Режим отображения веб-приложения (требуется переустановка)",
"form.prefs.label.display_mode": "Режим отображения Progressive Web App (PWA)",
"form.prefs.select.older_first": "Сначала старые записи",
"form.prefs.select.recent_first": "Сначала последние записи",
"form.prefs.select.fullscreen": "Полноэкранный",
@@ -307,12 +309,19 @@
"form.prefs.select.browser": "Браузер",
"form.prefs.select.publish_time": "Время публикации заявки",
"form.prefs.select.created_time": "Время создания записи",
"form.prefs.select.alphabetical": "По алфавиту",
"form.prefs.select.unread_count": "Количество непрочитанных",
"form.prefs.select.none": "Никто",
"form.prefs.select.tap": "Двойное нажатие",
"form.prefs.select.swipe": "Проведите",
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
"form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве",
"form.prefs.label.entry_swipe": "Включить пролистывание ввода на сенсорных экранах",
"form.prefs.label.gesture_nav": "Жест для перехода между записями",
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
"form.prefs.label.custom_css": "Пользовательские CSS",
"form.prefs.label.entry_order": "Колонка сортировки ввода",
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
"form.import.label.file": "OPML файл",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Активировать Fever API",
@@ -334,6 +343,7 @@
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Подключить аккаунт Pocket",
"form.integration.wallabag_only_url": "Отправлять только URL (вместо всего содержимого)",
"form.integration.wallabag_activate": "Сохранять статьи в Wallabag",
"form.integration.wallabag_endpoint": "Конечная точка Wallabag API",
"form.integration.wallabag_client_id": "Wallabag Client ID",
@@ -353,6 +363,11 @@
"form.integration.linkding_activate": "Сохранять статьи в Linkding",
"form.integration.linkding_endpoint": "Конечная точка Linkding API",
"form.integration.linkding_api_key": "Linkding API key",
"form.integration.matrix_bot_activate": "Перенос новых статей в Матрицу",
"form.integration.matrix_bot_user": "Имя пользователя для Matrix",
"form.integration.matrix_bot_password": "Пароль для пользователя Matrix",
"form.integration.matrix_bot_url": "URL сервера Матрицы",
"form.integration.matrix_bot_chat_id": "ID комнаты Матрицы",
"form.api_key.label.description": "Описание API-ключа",
"form.submit.loading": "Загрузка…",
"form.submit.saving": "Сохранение…",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "Öğeyi en üste kaydır",
"page.keyboard_shortcuts.remove_feed": "Bu beslemeyi kaldır",
"page.keyboard_shortcuts.go_to_search": "Arama formuna odakla",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "İletişim kutusunu kapat",
"page.users.title": "Kullanıcılar",
"page.users.username": "Kullanıcı adı",
@@ -241,6 +242,7 @@
"error.invalid_timezone": "Geçersiz saat dilimi",
"error.invalid_entry_direction": "Geçersiz giriş yönü.",
"error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.",
"error.invalid_gesture_nav": "Hareketle gezinme geçersiz.",
"error.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!",
"error.empty_file": "Bu dosya boş.",
"error.bad_credentials": "Geçersiz kullanıcı veya parola.",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "Sayfa başına ileti",
"form.prefs.label.default_reading_speed": "Diğer diller için okuma hızı (dakika başına kelime)",
"form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
"form.prefs.label.display_mode": "Web uygulaması görüntüleme modu (yeniden kurulum gerektirir)",
"form.prefs.label.display_mode": "Aşamalı Web Uygulaması (PWA) görüntüleme modu",
"form.prefs.select.older_first": "Önce eski iletiler",
"form.prefs.select.recent_first": "Önce yeni iletiler",
"form.prefs.select.fullscreen": "Tam Ekran",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "Tarayıcı",
"form.prefs.select.publish_time": "Giriş yayınlanma zamanı",
"form.prefs.select.created_time": "Girişin oluşturulma zamanı",
"form.prefs.select.alphabetical": "Alfabetik",
"form.prefs.select.unread_count": "Okunmamış sayısı",
"form.prefs.select.none": "Hiçbiri",
"form.prefs.select.tap": "çift dokunma",
"form.prefs.select.swipe": "Tokatlamak",
"form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
"form.prefs.label.entry_swipe": "Mobil cihazlarda iletiler için kaydırma hareketlerini etkinleştir",
"form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах",
"form.prefs.label.gesture_nav": "Girişler arasında gezinmek için hareket",
"form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
"form.prefs.label.custom_css": "Özel CSS",
"form.prefs.label.entry_order": "Giriş Sıralama Sütunu",
"form.prefs.label.default_home_page": "Varsayılan ana sayfa",
"form.prefs.label.categories_sorting_order": "Kategoriler sıralama",
"form.import.label.file": "OPML dosyası",
"form.import.label.url": "URL",
"form.integration.fever_activate": "Fever API'yi Etkinleştir",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Pocket hesabını bağla",
"form.integration.wallabag_activate": "Makaleleri Wallabag'e kaydet",
"form.integration.wallabag_only_url": "Yalnızca URL gönder (tam içerik yerine)",
"form.integration.wallabag_endpoint": "Wallabag API Uç Noktası",
"form.integration.wallabag_client_id": "Wallabag Client ID",
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "Makaleleri Linkding'e kaydet",
"form.integration.linkding_endpoint": "Linkding API Uç Noktası",
"form.integration.linkding_api_key": "Linkding API Anahtarı",
"form.integration.matrix_bot_activate": "Yeni makaleleri Matrix'e aktarın",
"form.integration.matrix_bot_user": "Matrix için Kullanıcı Adı",
"form.integration.matrix_bot_password": "Matrix kullanıcısı için şifre",
"form.integration.matrix_bot_url": "Matris sunucusu URL'si",
"form.integration.matrix_bot_chat_id": "Matris odasının kimliği",
"form.api_key.label.description": "API Anahtar Etiketi",
"form.submit.loading": "Yükleniyor...",
"form.submit.saving": "Kaydediliyor...",
+388
View File
@@ -0,0 +1,388 @@
{
"confirm.question": "Ви впевнені?",
"confirm.yes": "так",
"confirm.no": "ні",
"confirm.loading": "В процесі...",
"action.subscribe": "Підписатись",
"action.save": "Зберегти",
"action.or": "або",
"action.cancel": "скасувати",
"action.remove": "Видалити",
"action.remove_feed": "Видалити стрічку",
"action.update": "Зберегти",
"action.edit": "Редагувати",
"action.download": "Завантажити",
"action.import": "Імпортувати",
"action.login": "Увійти",
"action.home_screen": "Додати до головного екрану",
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
"tooltip.logged_user": "Здійснено вхід як %s",
"menu.unread": "Непрочитане",
"menu.starred": "З зірочкою",
"menu.history": "Історія",
"menu.feeds": "Стрічки",
"menu.categories": "Категорії",
"menu.settings": "Налаштування",
"menu.logout": "Вийти",
"menu.preferences": "Уподобання",
"menu.integrations": "Інтеграції",
"menu.sessions": "Сеанси",
"menu.users": "Користувачі",
"menu.about": "Про додаток",
"menu.export": "Експорт",
"menu.import": "Імпорт",
"menu.create_category": "Створити категорію",
"menu.mark_page_as_read": "Відмітити цю сторінку як прочитане",
"menu.mark_all_as_read": "Відмітити все як прочитане",
"menu.show_all_entries": "Показати всі записи",
"menu.show_only_unread_entries": "Показати тільки непрочитані записи",
"menu.refresh_feed": "Оновити",
"menu.refresh_all_feeds": "Оновити всі стрічки у фоновому режимі",
"menu.edit_feed": "Редагувати",
"menu.edit_category": "Редагувати",
"menu.add_feed": "Додати підписку",
"menu.add_user": "Додати користувачв",
"menu.flush_history": "Очистити історію",
"menu.feed_entries": "Записи",
"menu.api_keys": "Ключі API",
"menu.create_api_key": "Створити новий ключ API",
"menu.shared_entries": "Спільні записи",
"search.label": "Пошук",
"search.placeholder": "Шукати...",
"pagination.next": "Вперед",
"pagination.previous": "Назад",
"entry.status.unread": "Непрочитане",
"entry.status.read": "Прочитане",
"entry.status.toast.unread": "Відмічено непрочитаним",
"entry.status.toast.read": "Відмічено прочитаним",
"entry.status.title": "Змінити стан запису",
"entry.bookmark.toggle.on": "Поставити зірочку",
"entry.bookmark.toggle.off": "Прибрати зірочку",
"entry.bookmark.toast.on": "З зірочкою",
"entry.bookmark.toast.off": "Без зірочки",
"entry.state.saving": "Зберігаю...",
"entry.state.loading": "Завантаження...",
"entry.save.label": "Зберегти",
"entry.save.title": "Зберегти цю статтю",
"entry.save.completed": "Готово!",
"entry.save.toast.completed": "Стаття збережена",
"entry.scraper.label": "Завантажити",
"entry.scraper.title": "Отримати оригінальний зміст",
"entry.scraper.completed": "Готово!",
"entry.external_link.label": "Зовнішнє посилання",
"entry.comments.label": "Коментарі",
"entry.comments.title": "Дивитися коментарі",
"entry.share.label": "Поділитись",
"entry.share.title": "Поділитись статтєю",
"entry.unshare.label": "Не ділитися",
"entry.shared_entry.title": "Відкрити публічне посилання",
"entry.shared_entry.label": "Поділитись",
"entry.estimated_reading_time": [
"читати %d хвилину",
"читати %d хвилини",
"читати %d хвилин"
],
"page.shared_entries.title": "Спильні записи",
"page.unread.title": "Непрочитане",
"page.starred.title": "З зірочкою",
"page.categories.title": "Категорії",
"page.categories.no_feed": "Немає стрічки.",
"page.categories.entries": "Статті",
"page.categories.feeds": "Підписки",
"page.categories.feed_count": [
"Містить %d стрічку.",
"Містить %d стрічки.",
"Містить %d стрічок."
],
"page.categories.unread_counter": "Кількість непрочитаних записів",
"page.new_category.title": "Нова категорія",
"page.new_user.title": "Новий користувач",
"page.edit_category.title": "Редагування категорії: %s",
"page.edit_user.title": "Редагування користувача: %s",
"page.feeds.title": "Стрічки",
"page.feeds.last_check": "Остання перевірка:",
"page.feeds.unread_counter": "Кількість непрочитаних записів",
"page.feeds.read_counter": "Кількість прочитаних записів",
"page.feeds.error_count": ["%d помилка", "%d помилки", "%d помилок"],
"page.history.title": "Історія",
"page.import.title": "Імпорт",
"page.search.title": "Результати пошуку",
"page.about.title": "Про додадок",
"page.about.credits": "Титри",
"page.about.version": "Версія:",
"page.about.build_date": "Дата побудови:",
"page.about.author": "Автор:",
"page.about.license": "Ліцензія:",
"page.about.global_config_options": "Параметри глобальної конфігурації",
"page.about.postgres_version": "Версія Postgres:",
"page.about.go_version": "Версія Go:",
"page.add_feed.title": "Нова підписка",
"page.add_feed.no_category": "Немає категорії. Ви маєте додати принаймні одну категорію.",
"page.add_feed.label.url": "URL",
"page.add_feed.submit": "Знайти підписку",
"page.add_feed.legend.advanced_options": "Розширені опції",
"page.add_feed.choose_feed": "Обрати підписку",
"page.edit_feed.title": "Редагування стрічки: %s",
"page.edit_feed.last_check": "Остання перевірка:",
"page.edit_feed.last_modified_header": "Заголовок LastModified:",
"page.edit_feed.etag_header": "Заголовок ETag:",
"page.edit_feed.no_header": "Немає",
"page.edit_feed.last_parsing_error": "Остання помилка аналізу",
"page.entry.attachments": "Додатки",
"page.keyboard_shortcuts.title": "Комбінації клавиш",
"page.keyboard_shortcuts.subtitle.sections": "Навігація по розділах",
"page.keyboard_shortcuts.subtitle.items": "Навігація по записах",
"page.keyboard_shortcuts.subtitle.pages": "Навігація по сторінках",
"page.keyboard_shortcuts.subtitle.actions": "Дії",
"page.keyboard_shortcuts.go_to_unread": "Перейти до непрочитаних",
"page.keyboard_shortcuts.go_to_starred": "Перейти до закладок",
"page.keyboard_shortcuts.go_to_history": "Перейти до історії",
"page.keyboard_shortcuts.go_to_feeds": "Перейти до стрічок",
"page.keyboard_shortcuts.go_to_categories": "Перейти до категорій",
"page.keyboard_shortcuts.go_to_settings": "Перейти до налаштувань",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Показати комбінації клавиш",
"page.keyboard_shortcuts.go_to_previous_item": "Перейти до попереднього запису",
"page.keyboard_shortcuts.go_to_next_item": "Перейти до наступного запису",
"page.keyboard_shortcuts.go_to_feed": "Перейти до стрічки",
"page.keyboard_shortcuts.go_to_previous_page": "Перейти до попередньої сторінки",
"page.keyboard_shortcuts.go_to_next_page": "Перейти до наступної сторінки",
"page.keyboard_shortcuts.open_item": "Відкрити виділений запис",
"page.keyboard_shortcuts.open_original": "Відкрити оригінальне посилання",
"page.keyboard_shortcuts.open_original_same_window": "Відкрити оригінальне посилання в поточній вкладці",
"page.keyboard_shortcuts.open_comments": "Відкрити посилання на коментарі",
"page.keyboard_shortcuts.open_comments_same_window": "Відкрити посилання на коментарі в поточній вкладці",
"page.keyboard_shortcuts.toggle_read_status_next": "Переключити статус читання, перейти до наступного",
"page.keyboard_shortcuts.toggle_read_status_prev": "Переключити статус читання, перейти до попереднього",
"page.keyboard_shortcuts.refresh_all_feeds": "Оновити всі стрічки в фоновому режимі",
"page.keyboard_shortcuts.mark_page_as_read": "Відмітити поточну сторінку як прочитане",
"page.keyboard_shortcuts.download_content": "Завантажити оригінальний зміст",
"page.keyboard_shortcuts.toggle_bookmark_status": "Переключити статус закладки",
"page.keyboard_shortcuts.save_article": "Зберегти статтю",
"page.keyboard_shortcuts.scroll_item_to_top": "Прокрутити запис догори",
"page.keyboard_shortcuts.remove_feed": "Видалити цю стрічку",
"page.keyboard_shortcuts.go_to_search": "Поставити фокус на поле пошуку",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "Закрити модальне діалогове вікно",
"page.users.title": "Користувачі",
"page.users.username": "Ім’я користувача",
"page.users.never_logged": "Ніколи",
"page.users.admin.yes": "Так",
"page.users.admin.no": "Ні",
"page.users.actions": "Дії",
"page.users.last_login": "Дата останнього входу",
"page.users.is_admin": "Адміністратор",
"page.settings.title": "Налаштування ",
"page.settings.link_google_account": "Підключити мій обліковий запис Google",
"page.settings.unlink_google_account": "Відключити мій обліковий запис Google",
"page.settings.link_oidc_account": "Підключити мій обліковий запис OpenID Connect",
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис OpenID Connect",
"page.login.title": "Вхід",
"page.login.google_signin": "Увійти через Google",
"page.login.oidc_signin": "Увійти через OpenID Connect",
"page.integrations.title": "Інтеграції",
"page.integration.miniflux_api": "Miniflux API",
"page.integration.miniflux_api_endpoint": "Адреса доступу API",
"page.integration.miniflux_api_username": "Ім’я користувача",
"page.integration.miniflux_api_password": "Пароль",
"page.integration.miniflux_api_password_value": "Пароль до вашого облікового запису",
"page.integration.bookmarklet": "Букмарклет",
"page.integration.bookmarklet.name": "Додати до Miniflux",
"page.integration.bookmarklet.instructions": "Перетягніть це посилання до своїх закладок.",
"page.integration.bookmarklet.help": "Це спеціальне посилання дозволяє підписатися на веб-сайт безпосередньо за допомогою закладки у вашому веб-браузері.",
"page.sessions.title": "Сеанси",
"page.sessions.table.date": "Дата",
"page.sessions.table.ip": "IP адреса",
"page.sessions.table.user_agent": "User Agent",
"page.sessions.table.actions": "Дії",
"page.sessions.table.current_session": "Поточний сеанс",
"page.api_keys.title": "Ключі API",
"page.api_keys.table.description": "Опис",
"page.api_keys.table.token": "Токен",
"page.api_keys.table.last_used_at": "Дата останнього використання",
"page.api_keys.table.created_at": "Дата створення",
"page.api_keys.table.actions": "Дії",
"page.api_keys.never_used": "Ніколи не використався",
"page.new_api_key.title": "Створити ключ API",
"page.offline.title": "Автономний режим",
"page.offline.message": "Ви офлайн",
"page.offline.refresh_page": "Спробуйте оновити сторінку",
"alert.no_shared_entry": "Немає спільного запису.",
"alert.no_bookmark": "Наразі закладки відсутні.",
"alert.no_category": "Немає категорії.",
"alert.no_category_entry": "У цій категорії немає записів.",
"alert.no_feed_entry": "У цій стрічці немає записів.",
"alert.no_feed": "У вас немає підписок.",
"alert.no_feed_in_category": "У цій категорії немає підписок.",
"alert.no_history": "Наразі історія порожня.",
"alert.feed_error": "З цією стрічкою трапилась помилка",
"alert.no_search_result": "Немає результатів для цього пошуку.",
"alert.no_unread_entry": "Немає непрочитаних статей.",
"alert.no_user": "Ви єдиний користувач.",
"alert.account_unlinked": "Тепер ваш зовнішній обліковий запис підключено!",
"alert.account_linked": "Тепер ваш зовнішній обліковий запис від’єднано!",
"alert.pocket_linked": "Тепер ваш обліковий запис Pocket підключено!",
"alert.prefs_saved": "Уподобання збережено!",
"error.unlink_account_without_password": "Ви маєте встановити пароль, щоб мати можливість увійти наступного разу",
"error.duplicate_linked_account": "Вже є обліковий запис, під’єднаний до цього провайдера!",
"error.duplicate_fever_username": "Вже є обліковий запис з таким самим користувачем Fever!",
"error.duplicate_googlereader_username": "Вже є обліковий запис з таким самим користувачем Google Reader!",
"error.pocket_request_token": "Не вдалося отримати токен доступу з Pocket!",
"error.pocket_access_token": "Не вдалося отримати токен доступу з Pocket!",
"error.category_already_exists": "Така категорія вже існує.",
"error.unable_to_create_category": "Не вдається сворити категорію.",
"error.unable_to_update_category": "Не вдається відредагувати категорію.",
"error.user_already_exists": "Такий користувач вже існує.",
"error.unable_to_create_user": "Не вдається створити користувача.",
"error.unable_to_update_user": "Не вдається оновити користувача.",
"error.unable_to_update_feed": "Не вдається оновити стрічку.",
"error.subscription_not_found": "Не знайшлося жодної підписки.",
"error.invalid_theme": "Недійсна тема.",
"error.invalid_language": "Недійсна мова.",
"error.invalid_timezone": "Недійсний часовий пояс.",
"error.invalid_entry_direction": "Недійсний напрямок запису.",
"error.invalid_display_mode": "Недійсний режим відображення.",
"error.invalid_gesture_nav": "Недійсна навігація жестами.",
"error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!",
"error.empty_file": "Цей файл порожній.",
"error.bad_credentials": "Невірне ім’я користувача або пароль.",
"error.fields_mandatory": "Всі поля є обов’язковими.",
"error.title_required": "Назва є обов’язковою.",
"error.different_passwords": "Паролі не співпадають.",
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
"error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
"error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
"error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
"error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
"error.feed_already_exists": "Така стрічка вже існує.",
"error.invalid_feed_url": "Недійсна URL-адреса стрічки.",
"error.invalid_site_url": "Недійсна URL-адреса сайту.",
"error.feed_url_not_empty": "URL-адреса стрічки не може бути порожньою.",
"error.site_url_not_empty": "URL-адреса сайту не може бути порожньою.",
"error.feed_title_not_empty": "Назва стрічки не може бути порожньою.",
"error.feed_category_not_found": "Категорія не існує або належить до іншого користувача.",
"error.feed_invalid_blocklist_rule": "Правило списку блокувань недійсне.",
"error.feed_invalid_keeplist_rule": "Правило списку дозволень недійсне.",
"error.user_mandatory_fields": "Ім’я користувача є обов’язковим.",
"error.api_key_already_exists": "Такий ключ API вже існує.",
"error.unable_to_create_api_key": "Не вдається створити такий ключ API",
"form.feed.label.title": "Назва",
"form.feed.label.site_url": "URL-адреса сайту",
"form.feed.label.feed_url": "URL-адреса стрічки",
"form.feed.label.category": "Категорія",
"form.feed.label.crawler": "Завантажувати оригінальний вміст",
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
"form.feed.label.feed_password": "Пароль для завантаження",
"form.feed.label.user_agent": "Назначити User Agent",
"form.feed.label.cookie": "Встановити кукі",
"form.feed.label.scraper_rules": "Правила Scraper",
"form.feed.label.rewrite_rules": "Правила Rewrite",
"form.feed.label.blocklist_rules": "Правила блокування",
"form.feed.label.keeplist_rules": "Правила дозволення",
"form.feed.label.urlrewrite_rules": "Правила перезапису URL-адрес",
"form.feed.label.ignore_http_cache": "Ігнорувати кеш HTTP",
"form.feed.label.allow_self_signed_certificates": "Дозволити сертифікати з власним підписом або недійсні",
"form.feed.label.fetch_via_proxy": "Використати проксі-сервер",
"form.feed.label.disabled": "Не оновлювати цю стрічку",
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
"form.category.label.title": "Назва",
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
"form.user.label.username": "Ім’я користувача",
"form.user.label.password": "Пароль",
"form.user.label.confirmation": "Підтверждення паролю",
"form.user.label.admin": "Адміністратор",
"form.prefs.label.language": "Мова",
"form.prefs.label.timezone": "Часовий пояс",
"form.prefs.label.theme": "Тема",
"form.prefs.label.entry_sorting": "Сортування записів",
"form.prefs.label.entries_per_page": "Кількість записів на сторінку",
"form.prefs.label.default_reading_speed": "Швидкість читання для інших мов (слів на хвилину)",
"form.prefs.label.cjk_reading_speed": "Швидкість читання для китайської, корейської та японської мови (символів на хвилину)",
"form.prefs.label.display_mode": "Режим відображення Progressive Web App (PWA).",
"form.prefs.select.older_first": "Старіші записи спочатку",
"form.prefs.select.recent_first": "Останні записи спочатку",
"form.prefs.select.fullscreen": "Повний екран",
"form.prefs.select.standalone": "Автономний",
"form.prefs.select.minimal_ui": "Мінімальний",
"form.prefs.select.browser": "Браузер",
"form.prefs.select.publish_time": "Дата публікації запису",
"form.prefs.select.created_time": "Дата створення запису",
"form.prefs.select.alphabetical": "За алфавітом",
"form.prefs.select.unread_count": "Кількість непрочитаних",
"form.prefs.select.none": "Жодного",
"form.prefs.select.tap": "Двічі натисніть",
"form.prefs.select.swipe": "Проведіть пальцем",
"form.prefs.label.keyboard_shortcuts": "Увімкнути комбінації клавиш",
"form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах",
"form.prefs.label.gesture_nav": "Жест для переходу між записами",
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
"form.prefs.label.custom_css": "Спеціальний CSS",
"form.prefs.label.entry_order": "Стовпець сортування записів",
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
"form.import.label.file": "Файл OPML",
"form.import.label.url": "URL-адреса",
"form.integration.fever_activate": "Увімкнути API Fever",
"form.integration.fever_username": "Ім’я користувача Fever",
"form.integration.fever_password": "Пароль Fever",
"form.integration.fever_endpoint": "Адреса доступу API Fever:",
"form.integration.googlereader_activate": "Увімкнути API Google Reader",
"form.integration.googlereader_username": "Ім’я користувача Google Reader",
"form.integration.googlereader_password": "Пароль Google Reader",
"form.integration.googlereader_endpoint": "Адреса доступу API Google Reader:",
"form.integration.pinboard_activate": "Зберігати статті до Pinboard",
"form.integration.pinboard_token": "API ключ від Pinboard",
"form.integration.pinboard_tags": "Теги для Pinboard",
"form.integration.pinboard_bookmark": "Відмічати закладку як непрочитану",
"form.integration.instapaper_activate": "Зберігати статті до Instapaper",
"form.integration.instapaper_username": "Ім’я користувача Instapaper",
"form.integration.instapaper_password": "Пароль Instapaper",
"form.integration.pocket_activate": "Зберігати статті до Pocket",
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
"form.integration.pocket_access_token": "Pocket Access Token",
"form.integration.pocket_connect_link": "Підключити ваш обліковий запис Pocket",
"form.integration.wallabag_activate": "Зберігати статті до Wallabag",
"form.integration.wallabag_only_url": "Надіслати лише URL (замість повного вмісту)",
"form.integration.wallabag_endpoint": "Wallabag API Endpoint",
"form.integration.wallabag_client_id": "Wallabag Client ID",
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
"form.integration.wallabag_username": "Ім’я користувача Wallabag",
"form.integration.wallabag_password": "Пароль Wallabag",
"form.integration.nunux_keeper_activate": "Зберігати статті до Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
"form.integration.nunux_keeper_api_key": "Ключ API Nunux Keeper",
"form.integration.espial_activate": "Зберігати статті до Espial",
"form.integration.espial_endpoint": "Espial API Endpoint",
"form.integration.espial_api_key": "Ключ API Espial",
"form.integration.espial_tags": "Теги для Espial",
"form.integration.telegram_bot_activate": "Відправляти нові статті до чату Telegram",
"form.integration.telegram_bot_token": "Токен боту",
"form.integration.telegram_chat_id": "ID чату",
"form.integration.linkding_activate": "Зберігати статті до Linkding",
"form.integration.linkding_endpoint": "Linkding API Endpoint",
"form.integration.linkding_api_key": "Ключ API Linkding",
"form.integration.matrix_bot_activate": "Перенесення нових статей в Матрицю",
"form.integration.matrix_bot_user": "Ім'я користувача для Matrix",
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
"form.integration.matrix_bot_url": "URL-адреса сервера Матриці",
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
"form.api_key.label.description": "Назва ключа API",
"form.submit.loading": "Завантаження...",
"form.submit.saving": "Зберігаю...",
"time_elapsed.not_yet": "ще ні",
"time_elapsed.yesterday": "вчора",
"time_elapsed.now": "прямо зараз",
"time_elapsed.minutes": [
"%d хвилину тому",
"%d хвилини тому",
"%d хвилин тому"
],
"time_elapsed.hours": ["%d годину тому", "%d години тому", "%d годин тому"],
"time_elapsed.days": ["%d день тому", "%d дні тому", "%d днів тому"],
"time_elapsed.weeks": ["%d тиждень тому", "%d тижня тому", "%d тижнів тому"],
"time_elapsed.months": [
"%d місяць тому",
"%d місяця тому",
"%d місяців тому"
],
"time_elapsed.years": ["%d рік тому", "%d роки тому", "%d років тому"]
}
+17 -2
View File
@@ -160,6 +160,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "滚动到顶部",
"page.keyboard_shortcuts.remove_feed": "删除此源",
"page.keyboard_shortcuts.go_to_search": "将焦点放在搜索表单上",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "关闭对话窗口",
"page.users.title": "用户",
"page.users.username": "用户名",
@@ -261,6 +262,7 @@
"error.invalid_timezone": "无效的时区。",
"error.invalid_entry_direction": "无效的输入方向。",
"error.invalid_display_mode": "无效的网页应用显示模式。",
"error.invalid_gesture_nav": "手势导航无效。",
"error.invalid_default_home_page": "无效的默认主页!",
"form.feed.label.title": "标题",
"form.feed.label.site_url": "源网站 URL",
@@ -292,7 +294,7 @@
"form.prefs.label.theme": "主题",
"form.prefs.label.entry_sorting": "文章排序",
"form.prefs.label.entries_per_page": "每页文章数",
"form.prefs.label.display_mode": "渐进式网应用显示模式(需要重新添加)",
"form.prefs.label.display_mode": "渐进式网应用程序 (PWA) 显示模式",
"form.prefs.label.default_reading_speed": "其他语言的阅读速度(每分钟字数)",
"form.prefs.label.cjk_reading_speed": "中文、韩文和日文的阅读速度(每分钟字符数)",
"form.prefs.select.older_first": "旧->新",
@@ -303,12 +305,19 @@
"form.prefs.select.browser": "浏览器",
"form.prefs.select.publish_time": "文章发布时间",
"form.prefs.select.created_time": "文章创建时间",
"form.prefs.select.alphabetical": "按字母顺序",
"form.prefs.select.unread_count": "未读计数",
"form.prefs.select.none": "没有任何",
"form.prefs.select.tap": "双击",
"form.prefs.select.swipe": "滑动",
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
"form.prefs.label.entry_swipe": "在移动设备上启用滑动手势",
"form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动",
"form.prefs.label.gesture_nav": "在条目之间导航的手势",
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
"form.prefs.label.custom_css": "自定义 CSS",
"form.prefs.label.entry_order": "文章排序依据",
"form.prefs.label.default_home_page": "默认主页",
"form.prefs.label.categories_sorting_order": "分类排序",
"form.import.label.file": "OPML 文件",
"form.import.label.url": "URL",
"form.integration.fever_activate": "启用 Fever API",
@@ -331,6 +340,7 @@
"form.integration.pocket_access_token": "Pocket 访问密钥",
"form.integration.pocket_connect_link": "连接您的 Pocket 帐户",
"form.integration.wallabag_activate": "保存文章到 Wallabag",
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
"form.integration.wallabag_endpoint": "Wallabag URL",
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
"form.integration.wallabag_client_secret": "Wallabag 客户端 Secret",
@@ -349,6 +359,11 @@
"form.integration.linkding_activate": "保存文章到 Linkding",
"form.integration.linkding_endpoint": "Linkding API 端点",
"form.integration.linkding_api_key": "Linkding API 密钥",
"form.integration.matrix_bot_activate": "将新文章转移到 Matrix",
"form.integration.matrix_bot_user": "矩阵的用户名",
"form.integration.matrix_bot_password": "矩阵用户密码",
"form.integration.matrix_bot_url": "矩阵服务器 URL",
"form.integration.matrix_bot_chat_id": "Matrix房间ID",
"form.api_key.label.description": "API密钥标签",
"form.submit.loading": "载入中…",
"form.submit.saving": "保存中…",
+17 -2
View File
@@ -162,6 +162,7 @@
"page.keyboard_shortcuts.scroll_item_to_top": "滾動到頂部",
"page.keyboard_shortcuts.remove_feed": "刪除此Feed",
"page.keyboard_shortcuts.go_to_search": "將焦點放在搜尋表單上",
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
"page.keyboard_shortcuts.close_modal": "關閉對話視窗",
"page.users.title": "使用者",
"page.users.username": "使用者名稱",
@@ -263,6 +264,7 @@
"error.invalid_timezone": "無效的時區。",
"error.invalid_entry_direction": "無效的輸入方向。",
"error.invalid_display_mode": "無效的網頁應用顯示模式。",
"error.invalid_gesture_nav": "手勢導航無效.",
"error.invalid_default_home_page": "默認主頁無效!",
"form.feed.label.title": "標題",
"form.feed.label.site_url": "網站 URL",
@@ -296,7 +298,7 @@
"form.prefs.label.entries_per_page": "每頁文章數",
"form.prefs.label.default_reading_speed": "Reading speed for other languages (words per minute)",
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
"form.prefs.label.display_mode": "漸進式網應用顯示模式(需要重新新增)",
"form.prefs.label.display_mode": "漸進式網應用程序 (PWA) 顯示模式",
"form.prefs.select.older_first": "舊->新",
"form.prefs.select.recent_first": "新->舊",
"form.prefs.select.fullscreen": "全屏",
@@ -305,12 +307,19 @@
"form.prefs.select.browser": "瀏覽器",
"form.prefs.select.publish_time": "文章釋出時間",
"form.prefs.select.created_time": "文章建立時間",
"form.prefs.select.alphabetical": "按字母順序",
"form.prefs.select.unread_count": "未讀計數",
"form.prefs.select.none": "沒有任何",
"form.prefs.select.tap": "雙擊",
"form.prefs.select.swipe": "滑動",
"form.prefs.label.keyboard_shortcuts": "啟用鍵盤快捷鍵",
"form.prefs.label.entry_swipe": "在移動裝置上啟用滑動手勢",
"form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动",
"form.prefs.label.gesture_nav": "在條目之間導航的手勢",
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
"form.prefs.label.custom_css": "自定義 CSS",
"form.prefs.label.entry_order": "文章排序依據",
"form.prefs.label.default_home_page": "默認主頁",
"form.prefs.label.categories_sorting_order": "分類排序",
"form.import.label.file": "OPML 檔案",
"form.import.label.url": "URL",
"form.integration.fever_activate": "啟用 Fever API",
@@ -333,6 +342,7 @@
"form.integration.pocket_access_token": "Pocket 訪問金鑰",
"form.integration.pocket_connect_link": "連線您的 Pocket 帳戶",
"form.integration.wallabag_activate": "儲存文章到 Wallabag",
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
"form.integration.wallabag_endpoint": "Wallabag URL",
"form.integration.wallabag_client_id": "Wallabag 客戶端 ID",
"form.integration.wallabag_client_secret": "Wallabag 客戶端 Secret",
@@ -351,6 +361,11 @@
"form.integration.linkding_activate": "儲存文章到 Linkding",
"form.integration.linkding_endpoint": "Linkding API 端點",
"form.integration.linkding_api_key": "Linkding API 金鑰",
"form.integration.matrix_bot_activate": "將新文章轉移到 Matrix",
"form.integration.matrix_bot_user": "矩陣的用戶名",
"form.integration.matrix_bot_password": "矩陣用戶密碼",
"form.integration.matrix_bot_url": "矩陣服務器 URL",
"form.integration.matrix_bot_chat_id": "Matrix房間ID",
"form.api_key.label.description": "API金鑰標籤",
"form.submit.loading": "載入中…",
"form.submit.saving": "儲存中…",
+48 -3
View File
@@ -1,5 +1,5 @@
.\" Manpage for miniflux.
.TH "MINIFLUX" "1" "January 5, 2022" "\ \&" "\ \&"
.TH "MINIFLUX" "1" "August 29, 2022" "\ \&" "\ \&"
.SH NAME
miniflux \- Minimalist and opinionated feed reader
@@ -283,6 +283,26 @@ List of networks allowed to access the metrics endpoint (comma-separated values)
.br
Default is 127.0.0.1/8\&.
.TP
.B METRICS_USERNAME
Metrics endpoint username for basic HTTP authentication\&.
.br
Default is emtpty\&.
.TP
.B METRICS_USERNAME_FILE
Path to a file that contains the username for the metrics endpoint HTTP authentication\&.
.br
Default is emtpty\&.
.TP
.B METRICS_PASSWORD
Metrics endpoint password for basic HTTP authentication\&.
.br
Default is emtpty\&.
.TP
.B METRICS_PASSWORD_FILE
Path to a file that contains the password for the metrics endpoint HTTP authentication\&.
.br
Default is emtpty\&.
.TP
.B OAUTH2_PROVIDER
Possible values are "google" or "oidc"\&.
.br
@@ -365,11 +385,26 @@ Path to a secret key exposed as a file, it should contain $POCKET_CONSUMER_KEY v
.br
Default is empty\&.
.TP
.B PROXY_IMAGES
Avoids mixed content warnings for external images: http-only, all, or none\&.
.B PROXY_OPTION
Avoids mixed content warnings for external media: http-only, all, or none\&.
.br
Default is http-only\&.
.TP
.B PROXY_MEDIA_TYPES
A list of media types to proxify (comma-separated values): image, audio, video\&.
.br
Default is image only\&.
.TP
.B PROXY_HTTP_CLIENT_TIMEOUT
Time limit in seconds before the proxy HTTP client cancel the request\&.
.br
Default is 120 seconds\&.
.TP
.B PROXY_URL
Sets a server to proxy media through\&.
.br
Default is empty, miniflux does the proxying\&.
.TP
.B HTTP_CLIENT_TIMEOUT
Time limit in seconds before the HTTP client cancel the request\&.
.br
@@ -392,6 +427,11 @@ When empty, Miniflux uses a default User-Agent that includes the Miniflux versio
.br
Default is empty.
.TP
.B HTTP_SERVER_TIMEOUT
Time limit in seconds before the HTTP client cancel the request\&.
.br
Default is 300 seconds\&.
.TP
.B AUTH_PROXY_HEADER
Proxy authentication HTTP header\&.
.br
@@ -421,6 +461,11 @@ Enabled by default\&.
Set a custom invidious instance to use\&.
.br
Default is yewtu.be\&.
.TP
.B PROXY_PRIVATE_KEY
Set a custom custom private key used to sign proxified media url\&.
.br
Default is randomly generated at startup\&.
.SH AUTHORS
.P
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2022 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package model // import "miniflux.app/model"
func CategoriesSortingOptions() map[string]string {
return map[string]string{
"unread_count": "form.prefs.select.unread_count",
"alphabetical": "form.prefs.select.alphabetical",
}
}
+1
View File
@@ -37,6 +37,7 @@ type Entry struct {
ReadingTime int `json:"reading_time"`
Enclosures EnclosureList `json:"enclosures"`
Feed *Feed `json:"feed,omitempty"`
Tags []string `json:"tags"`
}
// Entries represents a list of entries.
+6
View File
@@ -21,6 +21,7 @@ type Integration struct {
GoogleReaderUsername string
GoogleReaderPassword string
WallabagEnabled bool
WallabagOnlyURL bool
WallabagURL string
WallabagClientID string
WallabagClientSecret string
@@ -42,4 +43,9 @@ type Integration struct {
LinkdingEnabled bool
LinkdingURL string
LinkdingAPIKey string
MatrixBotEnabled bool
MatrixBotUser string
MatrixBotPassword string
MatrixBotURL string
MatrixBotChatID string
}
+52 -40
View File
@@ -12,27 +12,29 @@ import (
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"`
CJKReadingSpeed int `json:"cjk_reading_speed"`
DefaultHomePage string `json:"default_home_page"`
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
GestureNav string `json:"gesture_nav"`
LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"`
CJKReadingSpeed int `json:"cjk_reading_speed"`
DefaultHomePage string `json:"default_home_page"`
CategoriesSortingOrder string `json:"categories_sorting_order"`
}
// UserCreationRequest represents the request to create a user.
@@ -46,25 +48,27 @@ type UserCreationRequest struct {
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
IsAdmin *bool `json:"is_admin"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"`
DefaultHomePage *string `json:"default_home_page"`
Username *string `json:"username"`
Password *string `json:"password"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
IsAdmin *bool `json:"is_admin"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
GestureNav *string `json:"gesture_nav"`
DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"`
DefaultHomePage *string `json:"default_home_page"`
CategoriesSortingOrder *string `json:"categories_sorting_order"`
}
// Patch updates the User object with the modification request.
@@ -129,6 +133,10 @@ func (u *UserModificationRequest) Patch(user *User) {
user.EntrySwipe = *u.EntrySwipe
}
if u.GestureNav != nil {
user.GestureNav = *u.GestureNav
}
if u.DisplayMode != nil {
user.DisplayMode = *u.DisplayMode
}
@@ -144,6 +152,10 @@ func (u *UserModificationRequest) Patch(user *User) {
if u.DefaultHomePage != nil {
user.DefaultHomePage = *u.DefaultHomePage
}
if u.CategoriesSortingOrder != nil {
user.CategoriesSortingOrder = *u.CategoriesSortingOrder
}
}
// UseTimezone converts last login date to the given timezone.
+4 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh
PKG_ARCH=`dpkg --print-architecture`
PKG_DATE=`date -R`
PKG_VERSION=`cd /src && git describe --tags --abbrev=0`
PKG_ARCH=$(dpkg --print-architecture)
PKG_DATE=$(date -R)
PKG_VERSION=$(cd /src && git describe --tags --abbrev=0)
echo "PKG_VERSION=$PKG_VERSION"
echo "PKG_ARCH=$PKG_ARCH"
@@ -22,6 +22,7 @@ cd /src && \
cp /src/packaging/debian/miniflux.manpages /build/debian/miniflux.manpages && \
cp /src/packaging/debian/miniflux.postinst /build/debian/miniflux.postinst && \
cp /src/packaging/debian/rules /build/debian/rules && \
cp /src/packaging/debian/miniflux.dirs /build/debian/miniflux.dirs && \
echo "miniflux ($PKG_VERSION) experimental; urgency=low" > /build/debian/changelog && \
echo " * Miniflux version $PKG_VERSION" >> /build/debian/changelog && \
echo " -- Frédéric Guillot <f@miniflux.net> $PKG_DATE" >> /build/debian/changelog && \
+2
View File
@@ -0,0 +1,2 @@
etc
usr/bin
-2
View File
@@ -9,8 +9,6 @@ override_dh_auto_clean:
override_dh_auto_test:
override_dh_auto_build:
override_dh_auto_install:
mkdir -p $(DESTDIR)/etc
mkdir -p $(DESTDIR)/usr/bin
cp miniflux.conf $(DESTDIR)/etc/miniflux.conf
cp miniflux $(DESTDIR)/usr/bin/miniflux
-70
View File
@@ -1,70 +0,0 @@
// Copyright 2020 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package proxy // import "miniflux.app/proxy"
import (
"strings"
"miniflux.app/config"
"miniflux.app/reader/sanitizer"
"miniflux.app/url"
"github.com/PuerkitoBio/goquery"
"github.com/gorilla/mux"
)
// ImageProxyRewriter replaces image URLs with internal proxy URLs.
func ImageProxyRewriter(router *mux.Router, data string) string {
proxyImages := config.Opts.ProxyImages()
if proxyImages == "none" {
return data
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
if err != nil {
return data
}
doc.Find("img").Each(func(i int, img *goquery.Selection) {
if srcAttrValue, ok := img.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyImages == "all" || !url.IsHTTPS(srcAttrValue)) {
img.SetAttr("src", ProxifyURL(router, srcAttrValue))
}
}
if srcsetAttrValue, ok := img.Attr("srcset"); ok {
proxifySourceSet(img, router, proxyImages, srcsetAttrValue)
}
})
doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) {
if srcsetAttrValue, ok := sourceElement.Attr("srcset"); ok {
proxifySourceSet(sourceElement, router, proxyImages, srcsetAttrValue)
}
})
output, err := doc.Find("body").First().Html()
if err != nil {
return data
}
return output
}
func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxyImages, srcsetAttrValue string) {
imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue)
for _, imageCandidate := range imageCandidates {
if !isDataURL(imageCandidate.ImageURL) && (proxyImages == "all" || !url.IsHTTPS(imageCandidate.ImageURL)) {
imageCandidate.ImageURL = ProxifyURL(router, imageCandidate.ImageURL)
}
}
element.SetAttr("srcset", imageCandidates.String())
}
func isDataURL(s string) bool {
return strings.HasPrefix(s, "data:")
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2020 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package proxy // import "miniflux.app/proxy"
import (
"strings"
"miniflux.app/config"
"miniflux.app/reader/sanitizer"
"miniflux.app/url"
"github.com/PuerkitoBio/goquery"
"github.com/gorilla/mux"
)
type urlProxyRewriter func(router *mux.Router, url string) string
// ProxyRewriter replaces media URLs with internal proxy URLs.
func ProxyRewriter(router *mux.Router, data string) string {
return genericProxyRewriter(router, ProxifyURL, data)
}
// AbsoluteProxyRewriter do the same as ProxyRewriter except it uses absolute URLs.
func AbsoluteProxyRewriter(router *mux.Router, host, data string) string {
proxifyFunction := func(router *mux.Router, url string) string {
return AbsoluteProxifyURL(router, host, url)
}
return genericProxyRewriter(router, proxifyFunction, data)
}
func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, data string) string {
proxyOption := config.Opts.ProxyOption()
if proxyOption == "none" {
return data
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
if err != nil {
return data
}
for _, mediaType := range config.Opts.ProxyMediaTypes() {
switch mediaType {
case "image":
doc.Find("img").Each(func(i int, img *goquery.Selection) {
if srcAttrValue, ok := img.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !url.IsHTTPS(srcAttrValue)) {
img.SetAttr("src", proxifyFunction(router, srcAttrValue))
}
}
if srcsetAttrValue, ok := img.Attr("srcset"); ok {
proxifySourceSet(img, router, proxifyFunction, proxyOption, srcsetAttrValue)
}
})
doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) {
if srcsetAttrValue, ok := sourceElement.Attr("srcset"); ok {
proxifySourceSet(sourceElement, router, proxifyFunction, proxyOption, srcsetAttrValue)
}
})
case "audio":
doc.Find("audio").Each(func(i int, audio *goquery.Selection) {
if srcAttrValue, ok := audio.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !url.IsHTTPS(srcAttrValue)) {
audio.SetAttr("src", proxifyFunction(router, srcAttrValue))
}
}
})
doc.Find("audio source").Each(func(i int, sourceElement *goquery.Selection) {
if srcAttrValue, ok := sourceElement.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !url.IsHTTPS(srcAttrValue)) {
sourceElement.SetAttr("src", proxifyFunction(router, srcAttrValue))
}
}
})
case "video":
doc.Find("video").Each(func(i int, video *goquery.Selection) {
if srcAttrValue, ok := video.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !url.IsHTTPS(srcAttrValue)) {
video.SetAttr("src", proxifyFunction(router, srcAttrValue))
}
}
})
doc.Find("video source").Each(func(i int, sourceElement *goquery.Selection) {
if srcAttrValue, ok := sourceElement.Attr("src"); ok {
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !url.IsHTTPS(srcAttrValue)) {
sourceElement.SetAttr("src", proxifyFunction(router, srcAttrValue))
}
}
})
}
}
output, err := doc.Find("body").First().Html()
if err != nil {
return data
}
return output
}
func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFunction urlProxyRewriter, proxyOption, srcsetAttrValue string) {
imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue)
for _, imageCandidate := range imageCandidates {
if !isDataURL(imageCandidate.ImageURL) && (proxyOption == "all" || !url.IsHTTPS(imageCandidate.ImageURL)) {
imageCandidate.ImageURL = proxifyFunction(router, imageCandidate.ImageURL)
}
}
element.SetAttr("srcset", imageCandidates.String())
}
func isDataURL(s string) bool {
return strings.HasPrefix(s, "data:")
}
@@ -15,7 +15,9 @@ import (
func TestProxyFilterWithHttpDefault(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "http-only")
os.Setenv("PROXY_OPTION", "http-only")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -25,11 +27,11 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.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 := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
output := ProxyRewriter(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
@@ -38,7 +40,8 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
func TestProxyFilterWithHttpsDefault(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "http-only")
os.Setenv("PROXY_OPTION", "http-only")
os.Setenv("PROXY_MEDIA_TYPES", "image")
var err error
parser := config.NewParser()
@@ -48,10 +51,10 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
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 := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
if expected != output {
@@ -61,7 +64,7 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
func TestProxyFilterWithHttpNever(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "none")
os.Setenv("PROXY_OPTION", "none")
var err error
parser := config.NewParser()
@@ -71,10 +74,10 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.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 := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
expected := input
if expected != output {
@@ -84,7 +87,7 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
func TestProxyFilterWithHttpsNever(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "none")
os.Setenv("PROXY_OPTION", "none")
var err error
parser := config.NewParser()
@@ -94,10 +97,10 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
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 := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
expected := input
if expected != output {
@@ -107,7 +110,9 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
func TestProxyFilterWithHttpAlways(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -117,11 +122,11 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.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 := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
output := ProxyRewriter(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
@@ -130,7 +135,9 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
func TestProxyFilterWithHttpsAlways(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -140,11 +147,36 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
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 := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
output := ProxyRewriter(r, input)
expected := `<p><img src="/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
}
}
func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_URL", "https://proxy-example/proxy")
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 := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := ProxyRewriter(r, input)
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
@@ -153,7 +185,8 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
func TestProxyFilterWithHttpInvalid(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "invalid")
os.Setenv("PROXY_OPTION", "invalid")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -163,11 +196,11 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.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 := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
output := ProxyRewriter(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
@@ -176,7 +209,8 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
func TestProxyFilterWithHttpsInvalid(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "invalid")
os.Setenv("PROXY_OPTION", "invalid")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -186,10 +220,10 @@ func TestProxyFilterWithHttpsInvalid(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
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 := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
if expected != output {
@@ -199,7 +233,9 @@ func TestProxyFilterWithHttpsInvalid(t *testing.T) {
func TestProxyFilterWithSrcset(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -209,11 +245,11 @@ func TestProxyFilterWithSrcset(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="http://website/folder/image.png" srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w" alt="test"></p>`
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
output := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
@@ -222,7 +258,9 @@ func TestProxyFilterWithSrcset(t *testing.T) {
func TestProxyFilterWithEmptySrcset(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -232,11 +270,11 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="http://website/folder/image.png" srcset="" alt="test"></p>`
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="" alt="test"/></p>`
output := ImageProxyRewriter(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="" alt="test"/></p>`
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
@@ -245,7 +283,9 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
func TestProxyFilterWithPictureSource(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -255,11 +295,11 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<picture><source srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w, https://website/some,image.png 2x"></picture>`
expected := `<picture><source srcset="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w, /proxy/aHR0cHM6Ly93ZWJzaXRlL3NvbWUsaW1hZ2UucG5n 2x"/></picture>`
output := ImageProxyRewriter(r, input)
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w, /proxy/ZIw0hv8WhSTls5aSqhnFaCXlUrKIqTnBRaY0-NaLnds=/aHR0cHM6Ly93ZWJzaXRlL3NvbWUsaW1hZ2UucG5n 2x"/></picture>`
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
@@ -268,7 +308,9 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "https")
os.Setenv("PROXY_OPTION", "https")
os.Setenv("PROXY_MEDIA_TYPES", "image")
os.Setenv("PROXY_PRIVATE_KEY", "test")
var err error
parser := config.NewParser()
@@ -278,20 +320,21 @@ func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<picture><source srcset="http://website/folder/image2.png 656w, https://website/some,image.png 2x"></picture>`
expected := `<picture><source srcset="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, https://website/some,image.png 2x"/></picture>`
output := ImageProxyRewriter(r, input)
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, https://website/some,image.png 2x"/></picture>`
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
}
}
func TestImageProxyWithImageDataURL(t *testing.T) {
func TestProxyWithImageDataURL(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
var err error
parser := config.NewParser()
@@ -301,20 +344,21 @@ func TestImageProxyWithImageDataURL(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<img src="data:image/gif;base64,test">`
expected := `<img src="data:image/gif;base64,test"/>`
output := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
}
}
func TestImageProxyWithImageSourceDataURL(t *testing.T) {
func TestProxyWithImageSourceDataURL(t *testing.T) {
os.Clearenv()
os.Setenv("PROXY_IMAGES", "all")
os.Setenv("PROXY_OPTION", "all")
os.Setenv("PROXY_MEDIA_TYPES", "image")
var err error
parser := config.NewParser()
@@ -324,11 +368,11 @@ func TestImageProxyWithImageSourceDataURL(t *testing.T) {
}
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
expected := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
output := ImageProxyRewriter(r, input)
output := ProxyRewriter(r, input)
if expected != output {
t.Errorf(`Not expected output: got %s`, output)
+51 -2
View File
@@ -5,17 +5,66 @@
package proxy // import "miniflux.app/proxy"
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"net/url"
"path"
"miniflux.app/http/route"
"github.com/gorilla/mux"
"miniflux.app/config"
)
// ProxifyURL generates an URL for a proxified resource.
// ProxifyURL generates a relative URL for a proxified resource.
func ProxifyURL(router *mux.Router, link string) string {
if link != "" {
return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
proxyImageUrl := config.Opts.ProxyUrl()
if proxyImageUrl == "" {
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
mac.Write([]byte(link))
digest := mac.Sum(nil)
return route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
}
proxyUrl, err := url.Parse(proxyImageUrl)
if err != nil {
return ""
}
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
return proxyUrl.String()
}
return ""
}
// AbsoluteProxifyURL generates an absolute URL for a proxified resource.
func AbsoluteProxifyURL(router *mux.Router, host, link string) string {
if link != "" {
proxyImageUrl := config.Opts.ProxyUrl()
if proxyImageUrl == "" {
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
mac.Write([]byte(link))
digest := mac.Sum(nil)
path := route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
if config.Opts.HTTPS {
return "https://" + host + path
} else {
return "http://" + host + path
}
}
proxyUrl, err := url.Parse(proxyImageUrl)
if err != nil {
return ""
}
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
return proxyUrl.String()
}
return ""
}
+29 -8
View File
@@ -80,14 +80,15 @@ func (a *atom10Feed) Transform(baseURL string) *model.Feed {
}
type atom10Entry struct {
ID string `xml:"id"`
Title atom10Text `xml:"title"`
Published string `xml:"published"`
Updated string `xml:"updated"`
Links atomLinks `xml:"link"`
Summary atom10Text `xml:"summary"`
Content atom10Text `xml:"http://www.w3.org/2005/Atom content"`
Authors atomAuthors `xml:"author"`
ID string `xml:"id"`
Title atom10Text `xml:"title"`
Published string `xml:"published"`
Updated string `xml:"updated"`
Links atomLinks `xml:"link"`
Summary atom10Text `xml:"summary"`
Content atom10Text `xml:"http://www.w3.org/2005/Atom content"`
Authors atomAuthors `xml:"author"`
Categories []atom10Category `xml:"category"`
media.Element
}
@@ -101,6 +102,7 @@ func (a *atom10Entry) Transform() *model.Entry {
entry.Title = a.entryTitle()
entry.Enclosures = a.entryEnclosures()
entry.CommentsURL = a.entryCommentsURL()
entry.Tags = a.entryCategories()
return entry
}
@@ -214,6 +216,20 @@ func (a *atom10Entry) entryEnclosures() model.EnclosureList {
return enclosures
}
func (r *atom10Entry) entryCategories() []string {
var categoryList []string
for _, atomCategory := range r.Categories {
if strings.TrimSpace(atomCategory.Label) != "" {
categoryList = append(categoryList, strings.TrimSpace(atomCategory.Label))
} else {
categoryList = append(categoryList, strings.TrimSpace(atomCategory.Term))
}
}
return categoryList
}
// See https://tools.ietf.org/html/rfc4685#section-4
// If the type attribute of the atom:link is omitted, its value is assumed to be "application/atom+xml".
// We accept only HTML or XHTML documents for now since the intention is to have the same behavior as RSS.
@@ -232,6 +248,11 @@ type atom10Text struct {
XHTMLRootElement atomXHTMLRootElement `xml:"http://www.w3.org/1999/xhtml div"`
}
type atom10Category struct {
Term string `xml:"term,attr"`
Label string `xml:"label,attr"`
}
// Text: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.1
// HTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.2
// XHTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.3
+45
View File
@@ -1604,3 +1604,48 @@ func TestAbsoluteCommentsURL(t *testing.T) {
t.Errorf("Incorrect entry comments URL, got: %s", feed.Entries[0].CommentsURL)
}
}
func TestParseFeedWithCategories(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<link href="http://example.org/"/>
<author>
<name>Alice</name>
</author>
<author>
<name>Bob</name>
</author>
<entry>
<link href="http://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
<category term='Tech' />
<category term='Technology' label='Science' />
</entry>
</feed>`
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}
if len(feed.Entries[0].Tags) != 2 {
t.Errorf("Incorrect number of tags, got: %d", len(feed.Entries[0].Tags))
}
expected := "Tech"
result := feed.Entries[0].Tags[0]
if result != expected {
t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
}
expected = "Science"
result = feed.Entries[0].Tags[1]
if result != expected {
t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
}
}
+2
View File
@@ -43,6 +43,7 @@ type jsonItem struct {
Authors []jsonAuthor `json:"authors"`
Author jsonAuthor `json:"author"`
Attachments []jsonAttachment `json:"attachments"`
Tags []string `json:"tags"`
}
type jsonAttachment struct {
@@ -181,6 +182,7 @@ func (j *jsonItem) Transform() *model.Entry {
entry.Content = j.GetContent()
entry.Title = strings.TrimSpace(j.GetTitle())
entry.Enclosures = j.GetEnclosures()
entry.Tags = j.Tags
return entry
}
+42
View File
@@ -575,3 +575,45 @@ func TestParseInvalidJSON(t *testing.T) {
t.Error("Parse should returns an error")
}
}
func TestParseTags(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"user_comment": "This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json",
"title": "Brent Simmonss Microblog",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"author": {
"name": "Brent Simmons",
"url": "http://example.org/",
"avatar": "https://example.org/avatar.png"
},
"items": [
{
"id": "2347259",
"url": "https://example.org/2347259",
"content_text": "Cats are neat. \n\nhttps://example.org/cats",
"date_published": "2016-02-09T14:22:00-07:00",
"tags": [
"tag 1",
"tag 2"
]
}
]
}`
feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}
if len(feed.Entries[0].Tags) != 2 {
t.Errorf("Incorrect number of Tags, got: %d", len(feed.Entries[0].Tags))
}
expected := "tag 2"
result := feed.Entries[0].Tags[1]
if result != expected {
t.Errorf("Incorrect entry tag, got %q instead of %q", result, expected)
}
}
+21 -5
View File
@@ -41,7 +41,13 @@ var (
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.User) {
var filteredEntries model.Entries
for _, entry := range feed.Entries {
// array used for bulk push
entriesToPush := model.Entries{}
// Process older entries first
for i := len(feed.Entries) - 1; i >= 0; i-- {
entry := feed.Entries[i]
logger.Debug("[Processor] Processing entry %q from feed %q", entry.URL, feed.FeedURL)
if isBlockedEntry(feed, entry) || !isAllowedEntry(feed, entry) {
@@ -79,7 +85,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
}
}
entry.Content = rewrite.Rewriter(url, entry.Content, feed.RewriteRules)
rewrite.Rewriter(url, entry, feed.RewriteRules)
// The sanitizer should always run at the end of the process to make sure unsafe HTML is filtered.
entry.Content = sanitizer.Sanitize(url, entry.Content)
@@ -93,6 +99,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
go func() {
integration.PushEntry(localEntry, intg)
}()
entriesToPush = append(entriesToPush, localEntry)
}
}
@@ -100,6 +107,15 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
filteredEntries = append(filteredEntries, entry)
}
intg, err := store.Integration(feed.UserID)
if err != nil {
logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
} else if intg != nil && len(entriesToPush) > 0 {
go func() {
integration.PushEntries(entriesToPush, intg)
}()
}
feed.Entries = filteredEntries
}
@@ -152,14 +168,14 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User)
return scraperErr
}
content = rewrite.Rewriter(url, content, entry.Feed.RewriteRules)
content = sanitizer.Sanitize(url, content)
if content != "" {
entry.Content = content
entry.ReadingTime = calculateReadingTime(content, user)
}
rewrite.Rewriter(url, entry, entry.Feed.RewriteRules)
entry.Content = sanitizer.Sanitize(url, entry.Content)
return nil
}
+46
View File
@@ -335,3 +335,49 @@ func parseMarkdown(entryContent string) string {
return sb.String()
}
func removeTables(entryContent string) string {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(entryContent))
if err != nil {
return entryContent
}
selectors := []string{"table", "tbody", "thead", "td", "th", "td"}
var loopElement *goquery.Selection
for _, selector := range selectors {
for {
loopElement = doc.Find(selector).First()
if loopElement.Length() == 0 {
break
}
innerHtml, err := loopElement.Html()
if err != nil {
break
}
loopElement.Parent().AppendHtml(innerHtml)
loopElement.Remove()
}
}
output, _ := doc.Find("body").First().Html()
return output
}
func removeClickbait(entryTitle string) string {
titleWords := []string{}
for _, word := range strings.Fields(entryTitle) {
runes := []rune(word)
if len(runes) > 1 {
// keep first rune as is to keep the first capital letter
titleWords = append(titleWords, string([]rune{runes[0]})+strings.ToLower(string(runes[1:])))
} else {
titleWords = append(titleWords, word)
}
}
return strings.Join(titleWords, " ")
}
+26 -25
View File
@@ -10,6 +10,7 @@ import (
"text/scanner"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/url"
)
@@ -19,7 +20,7 @@ type rule struct {
}
// Rewriter modify item contents with a set of rewriting rules.
func Rewriter(entryURL, entryContent, customRewriteRules string) string {
func Rewriter(entryURL string, entry *model.Entry, customRewriteRules string) {
rulesList := getPredefinedRewriteRules(entryURL)
if customRewriteRules != "" {
rulesList = customRewriteRules
@@ -31,10 +32,8 @@ func Rewriter(entryURL, entryContent, customRewriteRules string) string {
logger.Debug(`[Rewrite] Applying rules %v for %q`, rules, entryURL)
for _, rule := range rules {
entryContent = applyRule(entryURL, entryContent, rule)
applyRule(entryURL, entry, rule)
}
return entryContent
}
func parseRules(rulesText string) (rules []rule) {
@@ -60,59 +59,61 @@ func parseRules(rulesText string) (rules []rule) {
}
}
func applyRule(entryURL, entryContent string, rule rule) string {
func applyRule(entryURL string, entry *model.Entry, rule rule) {
switch rule.name {
case "add_image_title":
entryContent = addImageTitle(entryURL, entryContent)
entry.Content = addImageTitle(entryURL, entry.Content)
case "add_mailto_subject":
entryContent = addMailtoSubject(entryURL, entryContent)
entry.Content = addMailtoSubject(entryURL, entry.Content)
case "add_dynamic_image":
entryContent = addDynamicImage(entryURL, entryContent)
entry.Content = addDynamicImage(entryURL, entry.Content)
case "add_youtube_video":
entryContent = addYoutubeVideo(entryURL, entryContent)
entry.Content = addYoutubeVideo(entryURL, entry.Content)
case "add_invidious_video":
entryContent = addInvidiousVideo(entryURL, entryContent)
entry.Content = addInvidiousVideo(entryURL, entry.Content)
case "add_youtube_video_using_invidious_player":
entryContent = addYoutubeVideoUsingInvidiousPlayer(entryURL, entryContent)
entry.Content = addYoutubeVideoUsingInvidiousPlayer(entryURL, entry.Content)
case "add_youtube_video_from_id":
entryContent = addYoutubeVideoFromId(entryContent)
entry.Content = addYoutubeVideoFromId(entry.Content)
case "add_pdf_download_link":
entryContent = addPDFLink(entryURL, entryContent)
entry.Content = addPDFLink(entryURL, entry.Content)
case "nl2br":
entryContent = replaceLineFeeds(entryContent)
entry.Content = replaceLineFeeds(entry.Content)
case "convert_text_link", "convert_text_links":
entryContent = replaceTextLinks(entryContent)
entry.Content = replaceTextLinks(entry.Content)
case "fix_medium_images":
entryContent = fixMediumImages(entryURL, entryContent)
entry.Content = fixMediumImages(entryURL, entry.Content)
case "use_noscript_figure_images":
entryContent = useNoScriptImages(entryURL, entryContent)
entry.Content = useNoScriptImages(entryURL, entry.Content)
case "replace":
// Format: replace("search-term"|"replace-term")
if len(rule.args) >= 2 {
entryContent = replaceCustom(entryContent, rule.args[0], rule.args[1])
entry.Content = replaceCustom(entry.Content, rule.args[0], rule.args[1])
} else {
logger.Debug("[Rewrite] Cannot find search and replace terms for replace rule %s", rule)
}
case "remove":
// Format: remove("#selector > .element, .another")
if len(rule.args) >= 1 {
entryContent = removeCustom(entryContent, rule.args[0])
entry.Content = removeCustom(entry.Content, rule.args[0])
} else {
logger.Debug("[Rewrite] Cannot find selector for remove rule %s", rule)
}
case "add_castopod_episode":
entryContent = addCastopodEpisode(entryURL, entryContent)
entry.Content = addCastopodEpisode(entryURL, entry.Content)
case "base64_decode":
if len(rule.args) >= 1 {
entryContent = applyFuncOnTextContent(entryContent, rule.args[0], decodeBase64Content)
entry.Content = applyFuncOnTextContent(entry.Content, rule.args[0], decodeBase64Content)
} else {
entryContent = applyFuncOnTextContent(entryContent, "body", decodeBase64Content)
entry.Content = applyFuncOnTextContent(entry.Content, "body", decodeBase64Content)
}
case "parse_markdown":
entryContent = parseMarkdown(entryContent)
entry.Content = parseMarkdown(entry.Content)
case "remove_tables":
entry.Content = removeTables(entry.Content)
case "remove_clickbait":
entry.Title = removeClickbait(entry.Title)
}
return entryContent
}
func getPredefinedRewriteRules(entryURL string) string {
+341 -135
View File
@@ -8,6 +8,8 @@ import (
"reflect"
"strings"
"testing"
"miniflux.app/model"
)
func TestParseRules(t *testing.T) {
@@ -46,178 +48,301 @@ func TestReplaceTextLinks(t *testing.T) {
}
func TestRewriteWithNoMatchingRule(t *testing.T) {
output := Rewriter("https://example.org/article", `Some text.`, ``)
expected := `Some text.`
controlEntry := &model.Entry{
Title: `A title`,
Content: `Some text.`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `Some text.`,
}
Rewriter("https://example.org/article", testEntry, ``)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithYoutubeLink(t *testing.T) {
output := Rewriter("https://www.youtube.com/watch?v=1234", "Video Description", ``)
expected := `<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/1234" allowfullscreen></iframe><br>Video Description`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/1234" allowfullscreen></iframe><br>Video Description`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `Video Description`,
}
Rewriter("https://www.youtube.com/watch?v=1234", testEntry, ``)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithInexistingCustomRule(t *testing.T) {
output := Rewriter("https://www.youtube.com/watch?v=1234", `Video Description`, `some rule`)
expected := `Video Description`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `Video Description`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `Video Description`,
}
Rewriter("https://www.youtube.com/watch?v=1234", testEntry, `some rule`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLink(t *testing.T) {
description := `<img src="https://imgs.xkcd.com/comics/thermostat.png" title="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." />`
output := Rewriter("https://xkcd.com/1912/", description, ``)
expected := `<figure><img src="https://imgs.xkcd.com/comics/thermostat.png" alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you."/><figcaption><p>Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you.</p></figcaption></figure>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="https://imgs.xkcd.com/comics/thermostat.png" alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you."/><figcaption><p>Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you.</p></figcaption></figure>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://imgs.xkcd.com/comics/thermostat.png" title="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." />`,
}
Rewriter("https://xkcd.com/1912/", testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkHtmlInjection(t *testing.T) {
description := `<img src="https://imgs.xkcd.com/comics/thermostat.png" title="<foo>" alt="<foo>" />`
output := Rewriter("https://xkcd.com/1912/", description, ``)
expected := `<figure><img src="https://imgs.xkcd.com/comics/thermostat.png" alt="&lt;foo&gt;"/><figcaption><p>&lt;foo&gt;</p></figcaption></figure>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="https://imgs.xkcd.com/comics/thermostat.png" alt="&lt;foo&gt;"/><figcaption><p>&lt;foo&gt;</p></figcaption></figure>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://imgs.xkcd.com/comics/thermostat.png" title="<foo>" alt="<foo>" />`,
}
Rewriter("https://xkcd.com/1912/", testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkAndImageNoTitle(t *testing.T) {
description := `<img src="https://imgs.xkcd.com/comics/thermostat.png" alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." />`
output := Rewriter("https://xkcd.com/1912/", description, ``)
expected := description
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://imgs.xkcd.com/comics/thermostat.png" alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." />`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://imgs.xkcd.com/comics/thermostat.png" alt="Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you." />`,
}
Rewriter("https://xkcd.com/1912/", testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkAndNoImage(t *testing.T) {
description := "test"
output := Rewriter("https://xkcd.com/1912/", description, ``)
expected := description
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `test`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `test`,
}
Rewriter("https://xkcd.com/1912/", testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdAndNoImage(t *testing.T) {
description := "test"
output := Rewriter("https://xkcd.com/1912/", description, ``)
expected := description
controlEntry := &model.Entry{
Title: `A title`,
Content: `test`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `test`,
}
Rewriter("https://xkcd.com/1912/", testEntry, ``)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteMailtoLink(t *testing.T) {
description := `<a href="mailto:ryan@qwantz.com?subject=blah%20blah">contact</a>`
output := Rewriter("https://www.qwantz.com/", description, ``)
expected := `<a href="mailto:ryan@qwantz.com?subject=blah%20blah">contact [blah blah]</a>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<a href="mailto:ryan@qwantz.com?subject=blah%20blah">contact [blah blah]</a>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<a href="mailto:ryan@qwantz.com?subject=blah%20blah">contact</a>`,
}
Rewriter("https://www.qwantz.com/", testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithPDFLink(t *testing.T) {
description := "test"
output := Rewriter("https://example.org/document.pdf", description, ``)
expected := `<a href="https://example.org/document.pdf">PDF</a><br>test`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<a href="https://example.org/document.pdf">PDF</a><br>test`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `test`,
}
Rewriter("https://example.org/document.pdf", testEntry, ``)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithNoLazyImage(t *testing.T) {
description := `<img src="https://example.org/image.jpg" alt="Image"><noscript><p>Some text</p></noscript>`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := description
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://example.org/image.jpg" alt="Image"><noscript><p>Some text</p></noscript>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://example.org/image.jpg" alt="Image"><noscript><p>Some text</p></noscript>`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithLazyImage(t *testing.T) {
description := `<img src="" data-url="https://example.org/image.jpg" alt="Image"><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := `<img src="https://example.org/image.jpg" data-url="https://example.org/image.jpg" alt="Image"/><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://example.org/image.jpg" data-url="https://example.org/image.jpg" alt="Image"/><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="" data-url="https://example.org/image.jpg" alt="Image"><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithLazyDivImage(t *testing.T) {
description := `<div data-url="https://example.org/image.jpg" alt="Image"></div><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := `<img src="https://example.org/image.jpg" alt="Image"/><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="https://example.org/image.jpg" alt="Image"/><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<div data-url="https://example.org/image.jpg" alt="Image"></div><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithUnknownLazyNoScriptImage(t *testing.T) {
description := `<img src="" data-non-candidate="https://example.org/image.jpg" alt="Image"><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := `<img src="" data-non-candidate="https://example.org/image.jpg" alt="Image"/><img src="https://example.org/fallback.jpg" alt="Fallback"/>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="" data-non-candidate="https://example.org/image.jpg" alt="Image"/><img src="https://example.org/fallback.jpg" alt="Fallback"/>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="" data-non-candidate="https://example.org/image.jpg" alt="Image"><noscript><img src="https://example.org/fallback.jpg" alt="Fallback"></noscript>`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithLazySrcset(t *testing.T) {
description := `<img srcset="" data-srcset="https://example.org/image.jpg" alt="Image">`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := `<img srcset="https://example.org/image.jpg" data-srcset="https://example.org/image.jpg" alt="Image"/>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img srcset="https://example.org/image.jpg" data-srcset="https://example.org/image.jpg" alt="Image"/>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img srcset="" data-srcset="https://example.org/image.jpg" alt="Image">`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithImageAndLazySrcset(t *testing.T) {
description := `<img src="meow" srcset="" data-srcset="https://example.org/image.jpg" alt="Image">`
output := Rewriter("https://example.org/article", description, "add_dynamic_image")
expected := `<img src="meow" srcset="https://example.org/image.jpg" data-srcset="https://example.org/image.jpg" alt="Image"/>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="meow" srcset="https://example.org/image.jpg" data-srcset="https://example.org/image.jpg" alt="Image"/>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="meow" srcset="" data-srcset="https://example.org/image.jpg" alt="Image">`,
}
Rewriter("https://example.org/article", testEntry, "add_dynamic_image")
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestNewLineRewriteRule(t *testing.T) {
description := "A\nB\nC"
output := Rewriter("https://example.org/article", description, "nl2br")
expected := `A<br>B<br>C`
controlEntry := &model.Entry{
Title: `A title`,
Content: `A<br>B<br>C`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: "A\nB\nC",
}
Rewriter("https://example.org/article", testEntry, "nl2br")
if expected != output {
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestConvertTextLinkRewriteRule(t *testing.T) {
description := "Test: http://example.org/a/b"
output := Rewriter("https://example.org/article", description, "convert_text_link")
expected := `Test: <a href="http://example.org/a/b">http://example.org/a/b</a>`
controlEntry := &model.Entry{
Title: `A title`,
Content: `Test: <a href="http://example.org/a/b">http://example.org/a/b</a>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `Test: http://example.org/a/b`,
}
Rewriter("https://example.org/article", testEntry, "convert_text_link")
if expected != output {
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestMediumImage(t *testing.T) {
content := `
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img alt="Image for post" class="t u v if aj" src="https://miro.medium.com/max/2560/1*ephLSqSzQYLvb7faDwzRbw.jpeg" width="1280" height="720" srcset="https://miro.medium.com/max/552/1*ephLSqSzQYLvb7faDwzRbw.jpeg 276w, https://miro.medium.com/max/1104/1*ephLSqSzQYLvb7faDwzRbw.jpeg 552w, https://miro.medium.com/max/1280/1*ephLSqSzQYLvb7faDwzRbw.jpeg 640w, https://miro.medium.com/max/1400/1*ephLSqSzQYLvb7faDwzRbw.jpeg 700w" sizes="700px"/>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `
<figure class="ht hu hv hw hx hy cy cz paragraph-image">
<div class="hz ia ib ic aj">
<div class="cy cz hs">
@@ -235,93 +360,174 @@ func TestMediumImage(t *testing.T) {
</div>
</div>
</figure>
`
expected := `<img alt="Image for post" class="t u v if aj" src="https://miro.medium.com/max/2560/1*ephLSqSzQYLvb7faDwzRbw.jpeg" width="1280" height="720" srcset="https://miro.medium.com/max/552/1*ephLSqSzQYLvb7faDwzRbw.jpeg 276w, https://miro.medium.com/max/1104/1*ephLSqSzQYLvb7faDwzRbw.jpeg 552w, https://miro.medium.com/max/1280/1*ephLSqSzQYLvb7faDwzRbw.jpeg 640w, https://miro.medium.com/max/1400/1*ephLSqSzQYLvb7faDwzRbw.jpeg 700w" sizes="700px"/>`
output := Rewriter("https://example.org/article", content, "fix_medium_images")
output = strings.TrimSpace(output)
`,
}
Rewriter("https://example.org/article", testEntry, "fix_medium_images")
testEntry.Content = strings.TrimSpace(testEntry.Content)
if expected != output {
t.Errorf(`Not expected output: %s`, output)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteNoScriptImageWithoutNoScriptTag(t *testing.T) {
content := `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."><figcaption>MDN Logo</figcaption></figure>`
expected := `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."/><figcaption>MDN Logo</figcaption></figure>`
output := Rewriter("https://example.org/article", content, "use_noscript_figure_images")
output = strings.TrimSpace(output)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."/><figcaption>MDN Logo</figcaption></figure>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."><figcaption>MDN Logo</figcaption></figure>`,
}
Rewriter("https://example.org/article", testEntry, "use_noscript_figure_images")
testEntry.Content = strings.TrimSpace(testEntry.Content)
if expected != output {
t.Errorf(`Not expected output: %s`, output)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteNoScriptImageWithNoScriptTag(t *testing.T) {
content := `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."><noscript><img src="http://example.org/logo.svg"></noscript><figcaption>MDN Logo</figcaption></figure>`
expected := `<figure><img src="http://example.org/logo.svg"/><figcaption>MDN Logo</figcaption></figure>`
output := Rewriter("https://example.org/article", content, "use_noscript_figure_images")
output = strings.TrimSpace(output)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="http://example.org/logo.svg"/><figcaption>MDN Logo</figcaption></figure>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<figure><img src="https://developer.mozilla.org/static/img/favicon144.png" alt="The beautiful MDN logo."><noscript><img src="http://example.org/logo.svg"></noscript><figcaption>MDN Logo</figcaption></figure>`,
}
Rewriter("https://example.org/article", testEntry, "use_noscript_figure_images")
testEntry.Content = strings.TrimSpace(testEntry.Content)
if expected != output {
t.Errorf(`Not expected output: %s`, output)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteReplaceCustom(t *testing.T) {
content := `<img src="http://example.org/logo.svg"><img src="https://example.org/article/picture.svg">`
expected := `<img src="http://example.org/logo.svg"><img src="https://example.org/article/picture.png">`
output := Rewriter("https://example.org/article", content, `replace("article/(.*).svg"|"article/$1.png")`)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<img src="http://example.org/logo.svg"><img src="https://example.org/article/picture.png">`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<img src="http://example.org/logo.svg"><img src="https://example.org/article/picture.svg">`,
}
Rewriter("https://example.org/article", testEntry, `replace("article/(.*).svg"|"article/$1.png")`)
if expected != output {
t.Errorf(`Not expected output: %s`, output)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteRemoveCustom(t *testing.T) {
content := `<div>Lorem Ipsum <span class="spam">I dont want to see this</span><span class="ads keep">Super important info</span></div>`
expected := `<div>Lorem Ipsum <span class="ads keep">Super important info</span></div>`
output := Rewriter("https://example.org/article", content, `remove(".spam, .ads:not(.keep)")`)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum <span class="ads keep">Super important info</span></div>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum <span class="spam">I dont want to see this</span><span class="ads keep">Super important info</span></div>`,
}
Rewriter("https://example.org/article", testEntry, `remove(".spam, .ads:not(.keep)")`)
if expected != output {
t.Errorf(`Not expected output: %s`, output)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteAddCastopodEpisode(t *testing.T) {
output := Rewriter("https://podcast.demo/@demo/episodes/test", "Episode Description", `add_castopod_episode`)
expected := `<iframe width="650" frameborder="0" src="https://podcast.demo/@demo/episodes/test/embed/light"></iframe><br>Episode Description`
controlEntry := &model.Entry{
Title: `A title`,
Content: `<iframe width="650" frameborder="0" src="https://podcast.demo/@demo/episodes/test/embed/light"></iframe><br>Episode Description`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `Episode Description`,
}
Rewriter("https://podcast.demo/@demo/episodes/test", testEntry, `add_castopod_episode`)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteBase64Decode(t *testing.T) {
content := `VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=`
expected := `This is some base64 encoded content`
output := Rewriter("https://example.org/article", content, `base64_decode`)
controlEntry := &model.Entry{
Title: `A title`,
Content: `This is some base64 encoded content`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=`,
}
Rewriter("https://example.org/article", testEntry, `base64_decode`)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteBase64DecodeInHTML(t *testing.T) {
content := `<div>Lorem Ipsum not valid base64<span class="base64">VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=</span></div>`
expected := `<div>Lorem Ipsum not valid base64<span class="base64">This is some base64 encoded content</span></div>`
output := Rewriter("https://example.org/article", content, `base64_decode`)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum not valid base64<span class="base64">This is some base64 encoded content</span></div>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum not valid base64<span class="base64">VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=</span></div>`,
}
Rewriter("https://example.org/article", testEntry, `base64_decode`)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteBase64DecodeArgs(t *testing.T) {
content := `<div>Lorem Ipsum<span class="base64">VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=</span></div>`
expected := `<div>Lorem Ipsum<span class="base64">This is some base64 encoded content</span></div>`
output := Rewriter("https://example.org/article", content, `base64_decode(".base64")`)
controlEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum<span class="base64">This is some base64 encoded content</span></div>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<div>Lorem Ipsum<span class="base64">VGhpcyBpcyBzb21lIGJhc2U2NCBlbmNvZGVkIGNvbnRlbnQ=</span></div>`,
}
Rewriter("https://example.org/article", testEntry, `base64_decode(".base64")`)
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteRemoveTables(t *testing.T) {
controlEntry := &model.Entry{
Title: `A title`,
Content: `<p>Test</p><p>Hello World!</p><p>Test</p>`,
}
testEntry := &model.Entry{
Title: `A title`,
Content: `<table class="container"><tbody><tr><td><p>Test</p><table class="row"><tbody><tr><td><p>Hello World!</p></td><td><p>Test</p></td></tr></tbody></table></td></tr></tbody></table>`,
}
Rewriter("https://example.org/article", testEntry, `remove_tables`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRemoveClickbait(t *testing.T) {
controlEntry := &model.Entry{
Title: `This Is Amazing`,
Content: `Some description`,
}
testEntry := &model.Entry{
Title: `THIS IS AMAZING`,
Content: `Some description`,
}
Rewriter("https://example.org/article", testEntry, `remove_clickbait`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
+4
View File
@@ -10,12 +10,14 @@ package rewrite // import "miniflux.app/reader/rewrite"
var predefinedRules = map[string]string{
"abstrusegoose.com": "add_image_title",
"amazingsuperpowers.com": "add_image_title",
"blog.cloudflare.com": `add_image_title,remove("figure.kg-image-card figure.kg-image + img")`,
"blog.laravel.com": "parse_markdown",
"cowbirdsinlove.com": "add_image_title",
"drawingboardcomic.com": "add_image_title",
"exocomics.com": "add_image_title",
"framatube.org": "nl2br,convert_text_link",
"happletea.com": "add_image_title",
"ilpost.it": `remove(".art_tag, #audioPlayerArticle, .author-container, .caption, .ilpostShare, .lastRecents, #mc_embed_signup, .outbrain_inread, p:has(.leggi-anche), .youtube-overlay")`,
"imogenquest.net": "add_image_title",
"lukesurl.com": "add_image_title",
"medium.com": "fix_medium_images",
@@ -29,8 +31,10 @@ var predefinedRules = map[string]string{
"quantamagazine.org": `add_youtube_video_from_id, remove("h6:not(.byline,.post__title__kicker), #comments, .next-post__content, .footer__section, figure .outer--content, script")`,
"sentfromthemoon.com": "add_image_title",
"thedoghousediaries.com": "add_image_title",
"theverge.com": `add_dynamic_image, remove("div.duet--recirculation--related-list")`,
"treelobsters.com": "add_image_title",
"www.qwantz.com": "add_image_title,add_mailto_subject",
"www.recalbox.com": "parse_markdown",
"xkcd.com": "add_image_title",
"youtube.com": "add_youtube_video",
}
+98
View File
@@ -95,6 +95,10 @@ func TestParseRss2Sample(t *testing.T) {
if feed.Entries[0].Content != `How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.` {
t.Errorf("Incorrect entry content, got: %s", feed.Entries[0].Content)
}
if feed.Entries[1].URL != "http://liftoff.msfc.nasa.gov/2003/05/30.html#item572" {
t.Errorf("Incorrect entry URL, got: %s", feed.Entries[1].URL)
}
}
func TestParseFeedWithoutTitle(t *testing.T) {
@@ -230,6 +234,34 @@ func TestParseEntryWithoutLink(t *testing.T) {
}
}
func TestParseEntryWithOnlyGuidPermalink(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<link>https://example.org/</link>
<item>
<guid isPermaLink="true">https://example.org/some-article.html</guid>
</item>
<item>
<guid>https://example.org/another-article.html</guid>
</item>
</channel>
</rss>`
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}
if feed.Entries[0].URL != "https://example.org/some-article.html" {
t.Errorf("Incorrect entry link, got: %s", feed.Entries[0].URL)
}
if feed.Entries[1].URL != "https://example.org/another-article.html" {
t.Errorf("Incorrect entry link, got: %s", feed.Entries[1].URL)
}
}
func TestParseEntryWithAtomLink(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
@@ -1394,3 +1426,69 @@ func TestEntryDescriptionFromGooglePlayDescription(t *testing.T) {
t.Errorf(`Unexpected podcast content, got %q instead of %q`, result, expected)
}
}
func TestParseEntryWithCategoryAndInnerHTML(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>Example</title>
<link>https://example.org/</link>
<atom:link href="https://example.org/rss" type="application/rss+xml" rel="self"></atom:link>
<item>
<title>Test</title>
<link>https://example.org/item</link>
<category>Category 1</category>
<category>Category 2</category>
</item>
</channel>
</rss>`
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}
if len(feed.Entries[0].Tags) != 2 {
t.Errorf("Incorrect number of tags, got: %d", len(feed.Entries[0].Tags))
}
expected := "Category 2"
result := feed.Entries[0].Tags[1]
if result != expected {
t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
}
}
func TestParseEntryWithCategoryAndCDATA(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>Example</title>
<link>https://example.org/</link>
<atom:link href="https://example.org/rss" type="application/rss+xml" rel="self"></atom:link>
<item>
<title>Test</title>
<link>https://example.org/item</link>
<author>
by <![CDATA[Foo Bar]]>
</author>
<category>Sample Category</category>
</item>
</channel>
</rss>`
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}
if len(feed.Entries[0].Tags) != 1 {
t.Errorf("Incorrect number of tags, got: %d", len(feed.Entries[0].Tags))
}
expected := "Sample Category"
result := feed.Entries[0].Tags[0]
if result != expected {
t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
}
}
+38 -2
View File
@@ -118,6 +118,12 @@ func (r rssFeed) feedAuthor() string {
return sanitizer.StripTags(strings.TrimSpace(author))
}
type rssGUID struct {
XMLName xml.Name
Data string `xml:",chardata"`
IsPermaLink string `xml:"isPermaLink,attr"`
}
type rssLink struct {
XMLName xml.Name
Data string `xml:",chardata"`
@@ -150,6 +156,12 @@ type rssEnclosure struct {
Length string `xml:"length,attr"`
}
type rssCategory struct {
XMLName xml.Name
Data string `xml:",chardata"`
Inner string `xml:",innerxml"`
}
func (enclosure *rssEnclosure) Size() int64 {
if enclosure.Length == "" {
return 0
@@ -159,7 +171,7 @@ func (enclosure *rssEnclosure) Size() int64 {
}
type rssItem struct {
GUID string `xml:"guid"`
GUID rssGUID `xml:"guid"`
Title []rssTitle `xml:"title"`
Links []rssLink `xml:"link"`
Description string `xml:"description"`
@@ -167,6 +179,7 @@ type rssItem struct {
Authors []rssAuthor `xml:"author"`
CommentLinks []rssCommentLink `xml:"comments"`
EnclosureLinks []rssEnclosure `xml:"enclosure"`
Categories []rssCategory `xml:"category"`
DublinCoreElement
FeedBurnerElement
PodcastEntryElement
@@ -183,6 +196,8 @@ func (r *rssItem) Transform() *model.Entry {
entry.Content = r.entryContent()
entry.Title = r.entryTitle()
entry.Enclosures = r.entryEnclosures()
entry.Tags = r.entryCategories()
return entry
}
@@ -237,7 +252,7 @@ func (r *rssItem) entryAuthor() string {
}
func (r *rssItem) entryHash() string {
for _, value := range []string{r.GUID, r.entryURL()} {
for _, value := range []string{r.GUID.Data, r.entryURL()} {
if value != "" {
return crypto.Hash(value)
}
@@ -291,6 +306,13 @@ func (r *rssItem) entryURL() string {
}
}
// Specs: https://cyber.harvard.edu/rss/rss.html#ltguidgtSubelementOfLtitemgt
// isPermaLink is optional, its default value is true.
// If its value is false, the guid may not be assumed to be a url, or a url to anything in particular.
if r.GUID.IsPermaLink == "true" || r.GUID.IsPermaLink == "" {
return strings.TrimSpace(r.GUID.Data)
}
return ""
}
@@ -359,6 +381,20 @@ func (r *rssItem) entryEnclosures() model.EnclosureList {
return enclosures
}
func (r *rssItem) entryCategories() []string {
var categoryList []string
for _, rssCategory := range r.Categories {
if strings.Contains(rssCategory.Inner, "<![CDATA[") {
categoryList = append(categoryList, strings.TrimSpace(rssCategory.Data))
} else {
categoryList = append(categoryList, strings.TrimSpace(rssCategory.Inner))
}
}
return categoryList
}
func (r *rssItem) entryCommentsURL() string {
for _, commentLink := range r.CommentLinks {
if commentLink.XMLName.Space == "" {
+28 -18
View File
@@ -101,6 +101,7 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
var htmlAttrs, attrNames []string
var err error
var isImageLargerThanLayout bool
var isAnchorLink bool
if tagName == "img" {
imgWidth := getIntegerAttributeValue("width", attributes)
@@ -137,6 +138,9 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
}
} else if tagName == "img" && attribute.Key == "src" && isValidDataAttribute(attribute.Val) {
value = attribute.Val
} else if isAnchor("a", attribute) {
value = attribute.Val
isAnchorLink = true
} else {
value, err = url.AbsoluteURL(baseURL, value)
if err != nil {
@@ -153,10 +157,12 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
htmlAttrs = append(htmlAttrs, fmt.Sprintf(`%s="%s"`, attribute.Key, html.EscapeString(value)))
}
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName)
if len(extraAttrNames) > 0 {
attrNames = append(attrNames, extraAttrNames...)
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
if !isAnchorLink {
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName)
if len(extraAttrNames) > 0 {
attrNames = append(attrNames, extraAttrNames...)
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
}
}
return attrNames, strings.Join(htmlAttrs, " ")
@@ -370,9 +376,9 @@ func getTagAllowList() map[string][]string {
whitelist["audio"] = []string{"src"}
whitelist["video"] = []string{"poster", "height", "width", "src"}
whitelist["source"] = []string{"src", "type", "srcset", "sizes", "media"}
whitelist["dt"] = []string{}
whitelist["dd"] = []string{}
whitelist["dl"] = []string{}
whitelist["dt"] = []string{"id"}
whitelist["dd"] = []string{"id"}
whitelist["dl"] = []string{"id"}
whitelist["table"] = []string{}
whitelist["caption"] = []string{}
whitelist["thead"] = []string{}
@@ -380,12 +386,12 @@ func getTagAllowList() map[string][]string {
whitelist["tr"] = []string{}
whitelist["td"] = []string{"rowspan", "colspan"}
whitelist["th"] = []string{"rowspan", "colspan"}
whitelist["h1"] = []string{}
whitelist["h2"] = []string{}
whitelist["h3"] = []string{}
whitelist["h4"] = []string{}
whitelist["h5"] = []string{}
whitelist["h6"] = []string{}
whitelist["h1"] = []string{"id"}
whitelist["h2"] = []string{"id"}
whitelist["h3"] = []string{"id"}
whitelist["h4"] = []string{"id"}
whitelist["h5"] = []string{"id"}
whitelist["h6"] = []string{"id"}
whitelist["strong"] = []string{}
whitelist["em"] = []string{}
whitelist["code"] = []string{}
@@ -393,12 +399,12 @@ func getTagAllowList() map[string][]string {
whitelist["blockquote"] = []string{}
whitelist["q"] = []string{"cite"}
whitelist["p"] = []string{}
whitelist["ul"] = []string{}
whitelist["li"] = []string{}
whitelist["ol"] = []string{}
whitelist["ul"] = []string{"id"}
whitelist["li"] = []string{"id"}
whitelist["ol"] = []string{"id"}
whitelist["br"] = []string{}
whitelist["del"] = []string{}
whitelist["a"] = []string{"href", "title"}
whitelist["a"] = []string{"href", "title", "id"}
whitelist["figure"] = []string{}
whitelist["figcaption"] = []string{}
whitelist["cite"] = []string{}
@@ -408,7 +414,7 @@ func getTagAllowList() map[string][]string {
whitelist["wbr"] = []string{}
whitelist["dfn"] = []string{}
whitelist["sub"] = []string{}
whitelist["sup"] = []string{}
whitelist["sup"] = []string{"id"}
whitelist["var"] = []string{}
whitelist["samp"] = []string{}
whitelist["s"] = []string{}
@@ -492,6 +498,10 @@ func isValidDataAttribute(value string) bool {
return false
}
func isAnchor(tagName string, attribute html.Attribute) bool {
return tagName == "a" && attribute.Key == "href" && strings.HasPrefix(attribute.Val, "#")
}
func isPositiveInteger(value string) bool {
if number, err := strconv.Atoi(value); err == nil {
return number > 0
+10
View File
@@ -203,6 +203,16 @@ func TestIFrameWithChildElements(t *testing.T) {
}
}
func TestAnchorLink(t *testing.T) {
input := `<p>This link is <a href="#some-anchor">an anchor</a></p>`
expected := `<p>This link is <a href="#some-anchor">an anchor</a></p>`
output := Sanitize("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
}
func TestInvalidURLScheme(t *testing.T) {
input := `<p>This link is <a src="file:///etc/passwd">not valid</a></p>`
expected := `<p>This link is not valid</p>`
+7 -1
View File
@@ -8,6 +8,7 @@ package scraper // import "miniflux.app/reader/scraper"
// domain => CSS selectors
var predefinedRules = map[string]string{
"bbc.co.uk": "div.vxp-column--single, div.story-body__inner, ul.gallery-images__list",
"blog.cloudflare.com": "div.post-content",
"cbc.ca": ".story-content",
"darkreading.com": "#article-main:not(header)",
"developpez.com": "div[itemprop=articleBody]",
@@ -19,6 +20,7 @@ var predefinedRules = map[string]string{
"heise.de": "header .article-content__lead, header .article-image, div.article-layout__content.article-content",
"igen.fr": "section.corps",
"ikiwiki.iki.fi": ".page.group",
"ilpost.it": ".entry-content",
"ing.dk": "section.body",
"lapresse.ca": ".amorce, .entry",
"lemonde.fr": "article",
@@ -31,15 +33,20 @@ var predefinedRules = map[string]string{
"npr.org": "#storytext",
"oneindia.com": ".io-article-body",
"opensource.com": "div[property]",
"openingsource.org": "article.suxing-popup-gallery",
"osnews.com": "div.newscontent1",
"phoronix.com": "div.content",
"pseudo-sciences.org": "#art_main",
"quantamagazine.org": ".outer--content, figure, script",
"raywenderlich.com": "article",
"royalroad.com": ".author-note-portlet,.chapter-content",
"slate.fr": ".field-items",
"smbc-comics.com": "div#cc-comicbody, div#aftercomic",
"swordscomic.com": "img#comic-image, div#info-frame.tab-content-area",
"techcrunch.com": "div.article-entry",
"theoatmeal.com": "div#comic",
"theregister.com": "#top-col-story h2, #body",
"theverge.com": "h2.inline:nth-child(2),h2.duet--article--dangerously-set-cms-markup,figure.w-full,div.duet--article--article-body-component",
"turnoff.us": "article.post-content",
"universfreebox.com": "#corps_corps",
"version2.dk": "section.body",
@@ -47,5 +54,4 @@ var predefinedRules = map[string]string{
"wired.com": "main figure, article",
"zeit.de": ".summary, .article-body",
"zdnet.com": "div.storyBody",
"openingsource.org": "article.suxing-popup-gallery",
}
+2 -1
View File
@@ -47,6 +47,7 @@ func Fetch(websiteURL, rules, userAgent string, cookie string, allowSelfSignedCe
}
// The entry URL could redirect somewhere else.
sameSite := url.Domain(websiteURL) == url.Domain(response.EffectiveURL)
websiteURL = response.EffectiveURL
if rules == "" {
@@ -54,7 +55,7 @@ func Fetch(websiteURL, rules, userAgent string, cookie string, allowSelfSignedCe
}
var content string
if rules != "" {
if sameSite && rules != "" {
logger.Debug(`[Scraper] Using rules %q for %q`, rules, websiteURL)
content, err = scrapContent(response.Body, rules)
} else {
+43 -36
View File
@@ -87,12 +87,12 @@ func parseWebPage(websiteURL string, data io.Reader) (Subscriptions, *errors.Loc
if title, exists := s.Attr("title"); exists {
subscription.Title = title
} else {
subscription.Title = "Feed"
}
if feedURL, exists := s.Attr("href"); exists {
subscription.URL, _ = url.AbsoluteURL(websiteURL, feedURL)
if feedURL != "" {
subscription.URL, _ = url.AbsoluteURL(websiteURL, feedURL)
}
}
if subscription.Title == "" {
@@ -143,46 +143,53 @@ func parseYoutubeVideoPage(websiteURL string) string {
func tryWellKnownUrls(websiteURL, userAgent, cookie, username, password string) (Subscriptions, *errors.LocalizedError) {
var subscriptions Subscriptions
knownURLs := map[string]string{
"/atom.xml": "atom",
"/feed.xml": "atom",
"/feed/": "atom",
"/rss.xml": "rss",
"/rss/": "rss",
"atom.xml": "atom",
"feed.xml": "atom",
"feed/": "atom",
"rss.xml": "rss",
"rss/": "rss",
}
lastCharacter := websiteURL[len(websiteURL)-1:]
if lastCharacter == "/" {
websiteURL = websiteURL[:len(websiteURL)-1]
websiteURLRoot := url.RootURL(websiteURL)
baseURLs := []string{
// Look for knownURLs in the root.
websiteURLRoot,
}
// Look for knownURLs in current subdirectory, such as 'example.com/blog/'.
websiteURL, _ = url.AbsoluteURL(websiteURL, "./")
if websiteURL != websiteURLRoot {
baseURLs = append(baseURLs, websiteURL)
}
for knownURL, kind := range knownURLs {
fullURL, err := url.AbsoluteURL(websiteURL, knownURL)
if err != nil {
continue
}
for _, baseURL := range baseURLs {
for knownURL, kind := range knownURLs {
fullURL, err := url.AbsoluteURL(baseURL, knownURL)
if err != nil {
continue
}
clt := client.NewClientWithConfig(fullURL, config.Opts)
clt.WithCredentials(username, password)
clt.WithUserAgent(userAgent)
clt.WithCookie(cookie)
clt := client.NewClientWithConfig(fullURL, config.Opts)
clt.WithCredentials(username, password)
clt.WithUserAgent(userAgent)
clt.WithCookie(cookie)
// Some websites redirects unknown URLs to the home page.
// As result, the list of known URLs is returned to the subscription list.
// We don't want the user to choose between invalid feed URLs.
clt.WithoutRedirects()
// Some websites redirects unknown URLs to the home page.
// As result, the list of known URLs is returned to the subscription list.
// We don't want the user to choose between invalid feed URLs.
clt.WithoutRedirects()
response, err := clt.Get()
if err != nil {
continue
}
response, err := clt.Get()
if err != nil {
continue
}
if response != nil && response.StatusCode == 200 {
subscription := new(Subscription)
subscription.Type = kind
subscription.Title = fullURL
subscription.URL = fullURL
if subscription.URL != "" {
subscriptions = append(subscriptions, subscription)
if response != nil && response.StatusCode == 200 {
subscription := new(Subscription)
subscription.Type = kind
subscription.Title = fullURL
subscription.URL = fullURL
if subscription.URL != "" {
subscriptions = append(subscriptions, subscription)
}
}
}
}
+266 -1
View File
@@ -4,7 +4,10 @@
package subscription
import "testing"
import (
"strings"
"testing"
)
func TestFindYoutubeChannelFeed(t *testing.T) {
scenarios := map[string]string{
@@ -19,3 +22,265 @@ func TestFindYoutubeChannelFeed(t *testing.T) {
}
}
}
func TestParseWebPageWithRssFeed(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="http://example.org/rss" rel="alternate" type="application/rss+xml" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "Some Title" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/rss" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "rss" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithAtomFeed(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="http://example.org/atom.xml" rel="alternate" type="application/atom+xml" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "Some Title" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/atom.xml" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "atom" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithJSONFeed(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="http://example.org/feed.json" rel="alternate" type="application/feed+json" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "Some Title" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/feed.json" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "json" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithOldJSONFeedMimeType(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="http://example.org/feed.json" rel="alternate" type="application/json" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "Some Title" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/feed.json" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "json" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithRelativeFeedURL(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="/feed.json" rel="alternate" type="application/feed+json" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "Some Title" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/feed.json" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "json" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithEmptyTitle(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="/feed.json" rel="alternate" type="application/feed+json">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 1 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
if subscriptions[0].Title != "http://example.org/feed.json" {
t.Errorf(`Incorrect subscription title: %q`, subscriptions[0].Title)
}
if subscriptions[0].URL != "http://example.org/feed.json" {
t.Errorf(`Incorrect subscription URL: %q`, subscriptions[0].URL)
}
if subscriptions[0].Type != "json" {
t.Errorf(`Incorrect subscription type: %q`, subscriptions[0].Type)
}
}
func TestParseWebPageWithMultipleFeeds(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href="http://example.org/atom.xml" rel="alternate" type="application/atom+xml" title="Atom Feed">
<link href="http://example.org/feed.json" rel="alternate" type="application/feed+json" title="JSON Feed">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 2 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
}
func TestParseWebPageWithEmptyFeedURL(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link href rel="alternate" type="application/feed+json" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 0 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
}
func TestParseWebPageWithNoHref(t *testing.T) {
htmlPage := `
<!doctype html>
<html>
<head>
<link rel="alternate" type="application/feed+json" title="Some Title">
</head>
<body>
</body>
</html>`
subscriptions, err := parseWebPage("http://example.org/", strings.NewReader(htmlPage))
if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
}
if len(subscriptions) != 0 {
t.Fatal(`Incorrect number of subscriptions returned`)
}
}
+27 -6
View File
@@ -26,6 +26,7 @@ import (
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
@@ -36,9 +37,9 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
certDomain := config.Opts.CertDomain()
listenAddr := config.Opts.ListenAddr()
server := &http.Server{
ReadTimeout: 300 * time.Second,
WriteTimeout: 300 * time.Second,
IdleTimeout: 300 * time.Second,
ReadTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
WriteTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
IdleTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
Handler: setupHandler(store, pool),
}
@@ -128,6 +129,7 @@ func startAutoCertTLSServer(server *http.Server, certDomain string, store *stora
}
server.TLSConfig = tlsConfig()
server.TLSConfig.GetCertificate = certManager.GetCertificate
server.TLSConfig.NextProtos = []string{"h2", "http/1.1", acme.ALPNProto}
// Handle http-01 challenge.
s := &http.Server{
@@ -206,7 +208,7 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
// Returns a 404 if the client is not authorized to access the metrics endpoint.
if route.GetName() == "metrics" && !isAllowedToAccessMetricsEndpoint(r) {
logger.Error(`[Metrics] Client not allowed: %s`, request.ClientIP(r))
logger.Error(`[Metrics] [ClientIP=%s] Client not allowed (%s)`, request.ClientIP(r), r.RemoteAddr)
http.NotFound(w, r)
return
}
@@ -220,7 +222,24 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
}
func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
clientIP := net.ParseIP(request.ClientIP(r))
if config.Opts.MetricsUsername() != "" && config.Opts.MetricsPassword() != "" {
clientIP := request.ClientIP(r)
username, password, authOK := r.BasicAuth()
if !authOK {
logger.Info("[Metrics] [ClientIP=%s] No authentication header sent", clientIP)
return false
}
if username == "" || password == "" {
logger.Info("[Metrics] [ClientIP=%s] Empty username or password", clientIP)
return false
}
if username != config.Opts.MetricsUsername() || password != config.Opts.MetricsPassword() {
logger.Error("[Metrics] [ClientIP=%s] Invalid username or password", clientIP)
return false
}
}
for _, cidr := range config.Opts.MetricsAllowedNetworks() {
_, network, err := net.ParseCIDR(cidr)
@@ -228,7 +247,9 @@ func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
logger.Fatal(`[Metrics] Unable to parse CIDR %v`, err)
}
if network.Contains(clientIP) {
// We use r.RemoteAddr in this case because HTTP headers like X-Forwarded-For can be easily spoofed.
// The recommendation is to use HTTP Basic authentication.
if network.Contains(net.ParseIP(request.FindRemoteIP(r))) {
return true
}
}
+18 -3
View File
@@ -112,6 +112,11 @@ func (s *Storage) Categories(userID int64) (model.Categories, error) {
// CategoriesWithFeedCount returns all categories with the number of feeds.
func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error) {
user, err := s.UserByID(userID)
if err != nil {
return nil, err
}
query := `
SELECT
c.id,
@@ -126,11 +131,21 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
FROM categories c
WHERE
user_id=$1
ORDER BY
count_unread DESC,
c.title ASC
`
if user.CategoriesSortingOrder == "alphabetical" {
query = query + `
ORDER BY
c.title ASC
`
} else {
query = query + `
ORDER BY
count_unread DESC,
c.title ASC
`
}
rows, err := s.db.Query(query, userID)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch categories: %v`, err)
+21 -3
View File
@@ -119,7 +119,8 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
feed_id,
reading_time,
changed_at,
document_vectors
document_vectors,
tags
)
VALUES
(
@@ -134,7 +135,8 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
$9,
$10,
now(),
setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($6, ''), 500000)), 'B')
setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($6, ''), 500000)), 'B'),
$11
)
RETURNING
id, status
@@ -151,6 +153,7 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
entry.UserID,
entry.FeedID,
entry.ReadingTime,
pq.Array(removeDuplicates(entry.Tags)),
).Scan(&entry.ID, &entry.Status)
if err != nil {
@@ -183,7 +186,8 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
content=$4,
author=$5,
reading_time=$6,
document_vectors = setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($4, ''), 500000)), 'B')
document_vectors = setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($4, ''), 500000)), 'B'),
tags=$10
WHERE
user_id=$7 AND feed_id=$8 AND hash=$9
RETURNING
@@ -200,6 +204,7 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
entry.UserID,
entry.FeedID,
entry.Hash,
pq.Array(removeDuplicates(entry.Tags)),
).Scan(&entry.ID)
if err != nil {
@@ -535,3 +540,16 @@ func (s *Storage) UnshareEntry(userID int64, entryID int64) (err error) {
}
return
}
// removeDuplicate removes duplicate entries from a slice
func removeDuplicates[T string | int](sliceList []T) []T {
allKeys := make(map[T]bool)
list := []T{}
for _, item := range sliceList {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
+13
View File
@@ -135,6 +135,17 @@ func (e *EntryQueryBuilder) WithStatuses(statuses []string) *EntryQueryBuilder {
return e
}
// WithTags filter by a list of entry tags.
func (e *EntryQueryBuilder) WithTags(tags []string) *EntryQueryBuilder {
if len(tags) > 0 {
for _, cat := range tags {
e.conditions = append(e.conditions, fmt.Sprintf("$%d = ANY(e.tags)", len(e.args)+1))
e.args = append(e.args, cat)
}
}
return e
}
// WithoutStatus set the entry status that should not be returned.
func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
if status != "" {
@@ -250,6 +261,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
e.reading_time,
e.created_at,
e.changed_at,
e.tags,
f.title as feed_title,
f.feed_url,
f.site_url,
@@ -312,6 +324,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.ReadingTime,
&entry.CreatedAt,
&entry.ChangedAt,
pq.Array(&entry.Tags),
&entry.Feed.Title,
&entry.Feed.FeedURL,
&entry.Feed.SiteURL,
+99 -63
View File
@@ -125,6 +125,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
googlereader_username,
googlereader_password,
wallabag_enabled,
wallabag_only_url,
wallabag_url,
wallabag_client_id,
wallabag_client_secret,
@@ -145,7 +146,12 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
telegram_bot_chat_id,
linkding_enabled,
linkding_url,
linkding_api_key
linkding_api_key,
matrix_bot_enabled,
matrix_bot_user,
matrix_bot_password,
matrix_bot_url,
matrix_bot_chat_id
FROM
integrations
WHERE
@@ -168,6 +174,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.GoogleReaderUsername,
&integration.GoogleReaderPassword,
&integration.WallabagEnabled,
&integration.WallabagOnlyURL,
&integration.WallabagURL,
&integration.WallabagClientID,
&integration.WallabagClientSecret,
@@ -189,6 +196,11 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.LinkdingEnabled,
&integration.LinkdingURL,
&integration.LinkdingAPIKey,
&integration.MatrixBotEnabled,
&integration.MatrixBotUser,
&integration.MatrixBotPassword,
&integration.MatrixBotURL,
&integration.MatrixBotChatID,
)
switch {
case err == sql.ErrNoRows:
@@ -223,32 +235,38 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
fever_username=$9,
fever_token=$10,
wallabag_enabled=$11,
wallabag_url=$12,
wallabag_client_id=$13,
wallabag_client_secret=$14,
wallabag_username=$15,
wallabag_password=$16,
nunux_keeper_enabled=$17,
nunux_keeper_url=$18,
nunux_keeper_api_key=$19,
pocket_enabled=$20,
pocket_access_token=$21,
pocket_consumer_key=$22,
googlereader_enabled=$23,
googlereader_username=$24,
googlereader_password=$25,
telegram_bot_enabled=$26,
telegram_bot_token=$27,
telegram_bot_chat_id=$28,
espial_enabled=$29,
espial_url=$30,
espial_api_key=$31,
espial_tags=$32,
linkding_enabled=$33,
linkding_url=$34,
linkding_api_key=$35
wallabag_only_url=$12,
wallabag_url=$13,
wallabag_client_id=$14,
wallabag_client_secret=$15,
wallabag_username=$16,
wallabag_password=$17,
nunux_keeper_enabled=$18,
nunux_keeper_url=$19,
nunux_keeper_api_key=$20,
pocket_enabled=$21,
pocket_access_token=$22,
pocket_consumer_key=$23,
googlereader_enabled=$24,
googlereader_username=$25,
googlereader_password=$26,
telegram_bot_enabled=$27,
telegram_bot_token=$28,
telegram_bot_chat_id=$29,
espial_enabled=$30,
espial_url=$31,
espial_api_key=$32,
espial_tags=$33,
linkding_enabled=$34,
linkding_url=$35,
linkding_api_key=$36,
matrix_bot_enabled=$37,
matrix_bot_user=$38,
matrix_bot_password=$39,
matrix_bot_url=$40,
matrix_bot_chat_id=$41
WHERE
user_id=$36
user_id=$42
`
_, err = s.db.Exec(
query,
@@ -263,6 +281,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.FeverUsername,
integration.FeverToken,
integration.WallabagEnabled,
integration.WallabagOnlyURL,
integration.WallabagURL,
integration.WallabagClientID,
integration.WallabagClientSecret,
@@ -287,6 +306,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.LinkdingEnabled,
integration.LinkdingURL,
integration.LinkdingAPIKey,
integration.MatrixBotEnabled,
integration.MatrixBotUser,
integration.MatrixBotPassword,
integration.MatrixBotURL,
integration.MatrixBotChatID,
integration.UserID,
)
} else {
@@ -294,43 +318,49 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
UPDATE
integrations
SET
pinboard_enabled=$1,
pinboard_token=$2,
pinboard_tags=$3,
pinboard_mark_as_unread=$4,
instapaper_enabled=$5,
instapaper_username=$6,
instapaper_password=$7,
fever_enabled=$8,
fever_username=$9,
fever_token=$10,
wallabag_enabled=$11,
wallabag_url=$12,
wallabag_client_id=$13,
wallabag_client_secret=$14,
wallabag_username=$15,
wallabag_password=$16,
nunux_keeper_enabled=$17,
nunux_keeper_url=$18,
nunux_keeper_api_key=$19,
pocket_enabled=$20,
pocket_access_token=$21,
pocket_consumer_key=$22,
googlereader_enabled=$23,
googlereader_username=$24,
googlereader_password=$25,
telegram_bot_enabled=$26,
telegram_bot_token=$27,
telegram_bot_chat_id=$28,
espial_enabled=$29,
espial_url=$30,
espial_api_key=$31,
espial_tags=$32,
linkding_enabled=$33,
linkding_url=$34,
linkding_api_key=$35
WHERE
user_id=$36
pinboard_enabled=$1,
pinboard_token=$2,
pinboard_tags=$3,
pinboard_mark_as_unread=$4,
instapaper_enabled=$5,
instapaper_username=$6,
instapaper_password=$7,
fever_enabled=$8,
fever_username=$9,
fever_token=$10,
wallabag_enabled=$11,
wallabag_only_url=$12,
wallabag_url=$13,
wallabag_client_id=$14,
wallabag_client_secret=$15,
wallabag_username=$16,
wallabag_password=$17,
nunux_keeper_enabled=$18,
nunux_keeper_url=$19,
nunux_keeper_api_key=$20,
pocket_enabled=$21,
pocket_access_token=$22,
pocket_consumer_key=$23,
googlereader_enabled=$24,
googlereader_username=$25,
googlereader_password=$26,
telegram_bot_enabled=$27,
telegram_bot_token=$28,
telegram_bot_chat_id=$29,
espial_enabled=$30,
espial_url=$31,
espial_api_key=$32,
espial_tags=$33,
linkding_enabled=$34,
linkding_url=$35,
linkding_api_key=$36,
matrix_bot_enabled=$37,
matrix_bot_user=$38,
matrix_bot_password=$39,
matrix_bot_url=$40,
matrix_bot_chat_id=$41
WHERE
user_id=$42
`
_, err = s.db.Exec(
query,
@@ -345,6 +375,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.FeverUsername,
integration.FeverToken,
integration.WallabagEnabled,
integration.WallabagOnlyURL,
integration.WallabagURL,
integration.WallabagClientID,
integration.WallabagClientSecret,
@@ -369,6 +400,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.LinkdingEnabled,
integration.LinkdingURL,
integration.LinkdingAPIKey,
integration.MatrixBotEnabled,
integration.MatrixBotUser,
integration.MatrixBotPassword,
integration.MatrixBotURL,
integration.MatrixBotChatID,
integration.UserID,
)
}
+17
View File
@@ -45,6 +45,23 @@ func (s *Storage) NewUserBatch(userID int64, batchSize int) (jobs model.JobList,
return s.fetchBatchRows(fmt.Sprintf(query, batchSize), userID)
}
// NewCategoryBatch returns a series of jobs but only for a given category.
func (s *Storage) NewCategoryBatch(userID int64, categoryID int64, batchSize int) (jobs model.JobList, err error) {
// We do not take the error counter into consideration when the given
// user refresh manually all his feeds to force a refresh.
query := `
SELECT
id,
user_id
FROM
feeds
WHERE
user_id=$1 AND category_id=$2 AND disabled is false
ORDER BY next_check_at ASC LIMIT %d
`
return s.fetchBatchRows(fmt.Sprintf(query, batchSize), userID, categoryID)
}
func (s *Storage) fetchBatchRows(query string, args ...interface{}) (jobs model.JobList, err error) {
rows, err := s.db.Query(query, args...)
if err != nil {
+50 -24
View File
@@ -81,6 +81,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
keyboard_shortcuts,
show_reading_time,
entry_swipe,
gesture_nav,
stylesheet,
google_id,
openid_connect_id,
@@ -88,7 +89,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
entry_order,
default_reading_speed,
cjk_reading_speed,
default_home_page
default_home_page,
categories_sorting_order
`
tx, err := s.db.Begin()
@@ -116,6 +118,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.KeyboardShortcuts,
&user.ShowReadingTime,
&user.EntrySwipe,
&user.GestureNav,
&user.Stylesheet,
&user.GoogleID,
&user.OpenIDConnectID,
@@ -124,6 +127,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.DefaultReadingSpeed,
&user.CJKReadingSpeed,
&user.DefaultHomePage,
&user.CategoriesSortingOrder,
)
if err != nil {
tx.Rollback()
@@ -170,16 +174,18 @@ func (s *Storage) UpdateUser(user *model.User) error {
keyboard_shortcuts=$9,
show_reading_time=$10,
entry_swipe=$11,
stylesheet=$12,
google_id=$13,
openid_connect_id=$14,
display_mode=$15,
entry_order=$16,
default_reading_speed=$17,
cjk_reading_speed=$18,
default_home_page=$19
gesture_nav=$12,
stylesheet=$13,
google_id=$14,
openid_connect_id=$15,
display_mode=$16,
entry_order=$17,
default_reading_speed=$18,
cjk_reading_speed=$19,
default_home_page=$20,
categories_sorting_order=$21
WHERE
id=$20
id=$22
`
_, err = s.db.Exec(
@@ -195,6 +201,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.KeyboardShortcuts,
user.ShowReadingTime,
user.EntrySwipe,
user.GestureNav,
user.Stylesheet,
user.GoogleID,
user.OpenIDConnectID,
@@ -203,6 +210,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.DefaultReadingSpeed,
user.CJKReadingSpeed,
user.DefaultHomePage,
user.CategoriesSortingOrder,
user.ID,
)
if err != nil {
@@ -221,16 +229,18 @@ func (s *Storage) UpdateUser(user *model.User) error {
keyboard_shortcuts=$8,
show_reading_time=$9,
entry_swipe=$10,
stylesheet=$11,
google_id=$12,
openid_connect_id=$13,
display_mode=$14,
entry_order=$15,
default_reading_speed=$16,
cjk_reading_speed=$17,
default_home_page=$18
gesture_nav=$11,
stylesheet=$12,
google_id=$13,
openid_connect_id=$14,
display_mode=$15,
entry_order=$16,
default_reading_speed=$17,
cjk_reading_speed=$18,
default_home_page=$19,
categories_sorting_order=$20
WHERE
id=$19
id=$21
`
_, err := s.db.Exec(
@@ -245,6 +255,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.KeyboardShortcuts,
user.ShowReadingTime,
user.EntrySwipe,
user.GestureNav,
user.Stylesheet,
user.GoogleID,
user.OpenIDConnectID,
@@ -253,6 +264,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.DefaultReadingSpeed,
user.CJKReadingSpeed,
user.DefaultHomePage,
user.CategoriesSortingOrder,
user.ID,
)
@@ -289,6 +301,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
keyboard_shortcuts,
show_reading_time,
entry_swipe,
gesture_nav,
last_login_at,
stylesheet,
google_id,
@@ -297,7 +310,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
entry_order,
default_reading_speed,
cjk_reading_speed,
default_home_page
default_home_page,
categories_sorting_order
FROM
users
WHERE
@@ -321,6 +335,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
keyboard_shortcuts,
show_reading_time,
entry_swipe,
gesture_nav,
last_login_at,
stylesheet,
google_id,
@@ -329,7 +344,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
entry_order,
default_reading_speed,
cjk_reading_speed,
default_home_page
default_home_page,
categories_sorting_order
FROM
users
WHERE
@@ -353,6 +369,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
keyboard_shortcuts,
show_reading_time,
entry_swipe,
gesture_nav,
last_login_at,
stylesheet,
google_id,
@@ -361,7 +378,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
entry_order,
default_reading_speed,
cjk_reading_speed,
default_home_page
default_home_page,
categories_sorting_order
FROM
users
WHERE
@@ -392,6 +410,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.keyboard_shortcuts,
u.show_reading_time,
u.entry_swipe,
u.gesture_nav,
u.last_login_at,
u.stylesheet,
u.google_id,
@@ -400,7 +419,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.entry_order,
u.default_reading_speed,
u.cjk_reading_speed,
u.default_home_page
u.default_home_page,
u.categories_sorting_order
FROM
users u
LEFT JOIN
@@ -425,6 +445,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.KeyboardShortcuts,
&user.ShowReadingTime,
&user.EntrySwipe,
&user.GestureNav,
&user.LastLoginAt,
&user.Stylesheet,
&user.GoogleID,
@@ -434,6 +455,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.DefaultReadingSpeed,
&user.CJKReadingSpeed,
&user.DefaultHomePage,
&user.CategoriesSortingOrder,
)
if err == sql.ErrNoRows {
@@ -520,6 +542,7 @@ func (s *Storage) Users() (model.Users, error) {
keyboard_shortcuts,
show_reading_time,
entry_swipe,
gesture_nav,
last_login_at,
stylesheet,
google_id,
@@ -528,7 +551,8 @@ func (s *Storage) Users() (model.Users, error) {
entry_order,
default_reading_speed,
cjk_reading_speed,
default_home_page
default_home_page,
categories_sorting_order
FROM
users
ORDER BY username ASC
@@ -554,6 +578,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.KeyboardShortcuts,
&user.ShowReadingTime,
&user.EntrySwipe,
&user.GestureNav,
&user.LastLoginAt,
&user.Stylesheet,
&user.GoogleID,
@@ -563,6 +588,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.DefaultReadingSpeed,
&user.CJKReadingSpeed,
&user.DefaultHomePage,
&user.CategoriesSortingOrder,
)
if err != nil {
+11 -3
View File
@@ -61,17 +61,25 @@ func (f *funcMap) Map() template.FuncMap {
return template.HTML(str)
},
"proxyFilter": func(data string) string {
return proxy.ImageProxyRewriter(f.router, data)
return proxy.ProxyRewriter(f.router, data)
},
"proxyURL": func(link string) string {
proxyImages := config.Opts.ProxyImages()
proxyOption := config.Opts.ProxyOption()
if proxyImages == "all" || (proxyImages != "none" && !url.IsHTTPS(link)) {
if proxyOption == "all" || (proxyOption != "none" && !url.IsHTTPS(link)) {
return proxy.ProxifyURL(f.router, link)
}
return link
},
"mustBeProxyfied": func(mediaType string) bool {
for _, t := range config.Opts.ProxyMediaTypes() {
if t == mediaType {
return true
}
}
return false
},
"domain": func(websiteURL string) string {
return url.Domain(websiteURL)
},
+7 -7
View File
@@ -1,7 +1,7 @@
{{ define "feed_list" }}
<div class="items">
{{ range .feeds }}
<article role="article" class="item {{ if ne .ParsingErrorCount 0 }}feed-parsing-error{{ else if ne .UnreadCount 0 }}feed-has-unread{{ end }}">
<article role="article" class="item feed-item {{ if ne .ParsingErrorCount 0 }}feed-parsing-error{{ else if ne .UnreadCount 0 }}feed-has-unread{{ end }}">
<div class="item-header" dir="auto">
<span class="item-title">
{{ if and (.Icon) (gt .Icon.IconID 0) }}
@@ -19,21 +19,21 @@
</div>
<div class="item-meta">
<ul class="item-meta-info">
<li dir="auto">
<li class="item-meta-info-site-url" dir="auto">
<a href="{{ .SiteURL | safeURL }}" title="{{ .SiteURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ domain .SiteURL }}</a>
</li>
<li>
<li class="item-meta-info-checked-at">
{{ t "page.feeds.last_check" }} <time datetime="{{ isodate .CheckedAt }}" title="{{ isodate .CheckedAt }}">{{ elapsed $.user.Timezone .CheckedAt }}</time>
</li>
</ul>
<ul class="item-meta-icons">
<li>
<li class="item-meta-icons-refresh">
<a href="{{ route "refreshFeed" "feedID" .ID }}">{{ icon "refresh" }}<span class="icon-label">{{ t "menu.refresh_feed" }}</span></a>
</li>
<li>
<li class="item-meta-icons-edit">
<a href="{{ route "editFeed" "feedID" .ID }}">{{ icon "edit" }}<span class="icon-label">{{ t "menu.edit_feed" }}</span></a>
</li>
<li>
<li class="item-meta-icons-remove">
<a href="#"
data-confirm="true"
data-label-question="{{ t "confirm.question" }}"
@@ -43,7 +43,7 @@
data-url="{{ route "removeFeed" "feedID" .ID }}">{{ icon "delete" }}<span class="icon-label">{{ t "action.remove" }}</span></a>
</li>
{{ if .UnreadCount }}
<li>
<li class="item-meta-icons-mark-as-read">
<a href="#"
data-confirm="true"
data-label-question="{{ t "confirm.question" }}"
+8 -8
View File
@@ -1,10 +1,10 @@
{{ define "item_meta" }}
<div class="item-meta">
<ul class="item-meta-info">
<li>
<li class="item-meta-info-title">
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}" title="{{ .entry.Feed.SiteURL }}" data-feed-link="true">{{ truncate .entry.Feed.Title 35 }}</a>
</li>
<li>
<li class="item-meta-info-timestamp">
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed .user.Timezone .entry.Date }}</time>
</li>
{{ if and .user.ShowReadingTime (gt .entry.ReadingTime 0) }}
@@ -16,7 +16,7 @@
{{ end }}
</ul>
<ul class="item-meta-icons">
<li>
<li class="item-meta-icons-read">
<a href="#"
title="{{ t "entry.status.title" }}"
data-toggle-status="true"
@@ -26,7 +26,7 @@
data-value="{{ if eq .entry.Status "read" }}read{{ else }}unread{{ end }}"
>{{ if eq .entry.Status "read" }}{{ icon "unread" }}{{ else }}{{ icon "read" }}{{ end }}<span class="icon-label">{{ if eq .entry.Status "read" }}{{ t "entry.status.unread" }}{{ else }}{{ t "entry.status.read" }}{{ end }}</span></a>
</li>
<li>
<li class="item-meta-icons-star">
<a href="#"
data-toggle-bookmark="true"
data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
@@ -37,12 +37,12 @@
>{{ if .entry.Starred }}{{ icon "unstar" }}{{ else }}{{ icon "star" }}{{ end }}<span class="icon-label">{{ if .entry.Starred }}{{ t "entry.bookmark.toggle.off" }}{{ else }}{{ t "entry.bookmark.toggle.on" }}{{ end }}</span></a>
</li>
{{ if .entry.ShareCode }}
<li>
<li class="item-meta-icons-share">
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
title="{{ t "entry.shared_entry.title" }}"
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
</li>
<li>
<li class="item-meta-icons-delete">
<a href="#"
data-confirm="true"
data-url="{{ route "unshareEntry" "entryID" .entry.ID }}"
@@ -63,7 +63,7 @@
>{{ icon "save" }}<span class="icon-label">{{ t "entry.save.label" }}</span></a>
</li>
{{ end }}
<li>
<li class="item-meta-icons-external-url">
<a href="{{ .entry.URL | safeURL }}"
target="_blank"
rel="noopener noreferrer"
@@ -71,7 +71,7 @@
data-original-link="true">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
</li>
{{ if .entry.CommentsURL }}
<li>
<li class="item-meta-icons-comments">
<a href="{{ .entry.CommentsURL | safeURL }}"
title="{{ t "entry.comments.title" }}"
target="_blank"

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