mirror of
https://github.com/Athou/commafeed.git
synced 2026-10-24 05:59:11 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
689c20d367 | ||
|
|
43588df2b4 | ||
|
|
d2aaddea33 | ||
|
|
de233f8ae5 | ||
|
|
3179e74dc7 | ||
|
|
6f5e9f5d49 | ||
|
|
8a43fdf200 | ||
|
|
69051e5bbb | ||
|
|
c648e7e0ec | ||
|
|
c91cf376ad | ||
|
|
f4e07597d0 | ||
|
|
64b64d3340 | ||
|
|
57ea87871e | ||
|
|
6154d80bb9 | ||
|
|
ad85745f85 | ||
|
|
41fc77322c | ||
|
|
1b83c26417 | ||
|
|
10ea5f0b93 | ||
|
|
032684de68 | ||
|
|
3efec5ce58 | ||
|
|
4ed32964d9 | ||
|
|
05c1d0cc6e | ||
|
|
3c54924971 | ||
|
|
e57dfc0e3f | ||
|
|
eff8bb16a0 | ||
|
|
87e681b71e | ||
|
|
ac73ef4ea7 | ||
|
|
20945e7443 | ||
|
|
b7b1278111 | ||
|
|
6f9378404f | ||
|
|
79787aca6e | ||
|
|
2d90ccf856 | ||
|
|
7f77cf0268 | ||
|
|
f3c390e0e0 | ||
|
|
1fdc471197 | ||
|
|
ecf5d9e50e | ||
|
|
97a49e8faa | ||
|
|
f5a020cdc2 | ||
|
|
1f0507ee65 | ||
|
|
393c90f334 | ||
|
|
03ecc2f49b | ||
|
|
17743adce6 | ||
|
|
126dde103a | ||
|
|
14a25581b4 | ||
|
|
c7c95efb3a | ||
|
|
4c1293d357 | ||
|
|
76a92ea44c | ||
|
|
ba454322db | ||
|
|
a3b9f471e1 | ||
|
|
d58ddc888e | ||
|
|
fe9837c45c | ||
|
|
332d126bd7 | ||
|
|
0c66bf2695 | ||
|
|
4d6d07c98b | ||
|
|
8f7d4bfa10 | ||
|
|
7b3f39001f | ||
|
|
b9630809a0 | ||
|
|
ce3244da5e | ||
|
|
4067297d2a | ||
|
|
03c11fa181 | ||
|
|
3676e78eb9 | ||
|
|
1e08c4486f | ||
|
|
fd8bb60daa | ||
|
|
557d56ffc5 | ||
|
|
30f76a2349 | ||
|
|
cf0fa5e22e | ||
|
|
f16becbd82 | ||
|
|
4855450ddc | ||
|
|
0733aedce5 | ||
|
|
399ad14d9b | ||
|
|
7881ea3c5c | ||
|
|
7e9de622da | ||
|
|
c6655078f2 | ||
|
|
b86cef9499 | ||
|
|
5375ab6e97 | ||
|
|
75920a6473 | ||
|
|
f474e76866 | ||
|
|
d0ae27c4ce | ||
|
|
ab191dded6 | ||
|
|
950b571a9f | ||
|
|
7a153ca69a | ||
|
|
effd8946de | ||
|
|
ae3ad3745c |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
- name: Set up GraalVM
|
- name: Set up GraalVM
|
||||||
uses: graalvm/setup-graalvm@0def53c0fd8534bc13416c9469f5be45265824fd # v1.6.3
|
uses: graalvm/setup-graalvm@5298d94fb55a4f185c602eeac5de1b553882abe2 # v1.6.4
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: "graalvm"
|
distribution: "graalvm"
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: Install required packages
|
- name: Install required packages
|
||||||
run: sudo apt-get install -y rename unzip
|
run: sudo apt-get install -y rename unzip
|
||||||
|
|||||||
129
CHANGELOG.md
129
CHANGELOG.md
@@ -1,9 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [7.3.2]
|
||||||
|
|
||||||
|
- Prevent users from starring/tagging entries that are not theirs and subsequently reading them (GHSA-prfv-88mm-5gpg)
|
||||||
|
|
||||||
|
## [7.3.1]
|
||||||
|
|
||||||
|
- Prevent Host header injection attacks on the password recovery endpoint. A new setting
|
||||||
|
`commafeed.password-recovery-public-base-url` has been added to specify the base URL to use in password recovery
|
||||||
|
(GHSA-hp8h-jqfm-v7x5)
|
||||||
|
emails. This setting is only required when the password recovery feature is enabled, which is not the default.
|
||||||
|
|
||||||
|
## [7.3.0]
|
||||||
|
|
||||||
|
- Add support for the Google Reader API for native mobile apps that don't support the Fever API
|
||||||
|
- The profile menu now works again on mobile when action buttons are at the bottom of the screen
|
||||||
|
- More SSRF protection regarding IPv6 (GHSA-q5qw-345w-55xg)
|
||||||
|
- Changed the default value of `blockLocalAddresses` to make CommaFeed more secure out-of-the-box. See the "Access to
|
||||||
|
local address blocked" section of the README if you subscribe to feeds that are only available on your local network.
|
||||||
|
|
||||||
## [7.2.1]
|
## [7.2.1]
|
||||||
|
|
||||||
- correctly handle favicons hosted at the root of the domain for feeds with non-default ports
|
- correctly handle favicons hosted at the root of the domain for feeds with non-default ports
|
||||||
- prevent XXS injections in third party apps. The CommaFeed React client was already stripping malicious "javascript:" URLs, they are now filtered during feed parsing and no longer stored in the database/served through the REST API.
|
- prevent XXS injections in third party apps. The CommaFeed React client was already stripping malicious "javascript:"
|
||||||
|
URLs, they are now filtered during feed parsing and no longer stored in the database/served through the REST API
|
||||||
|
(GHSA-44pq-9929-f8mq)
|
||||||
|
|
||||||
## [7.2.0]
|
## [7.2.0]
|
||||||
|
|
||||||
@@ -13,26 +34,36 @@
|
|||||||
- Small performance improvements to the feed refresh engine
|
- Small performance improvements to the feed refresh engine
|
||||||
- Removed the Pocket sharing feature because the Pocket service has been discontinued
|
- Removed the Pocket sharing feature because the Pocket service has been discontinued
|
||||||
- Prevent the image proxy feature to access any URL to avoid SSRF attacks
|
- Prevent the image proxy feature to access any URL to avoid SSRF attacks
|
||||||
- Strengthened the SSRF protection by also blocking ULA and CGNAT ranges
|
- Strengthened the SSRF protection by also blocking ULA and CGNAT ranges (GHSA-hgrr-mjfp-gmr6)
|
||||||
- Prevent DDOS attacks by filtering OPML files during imports
|
- Prevent DDOS attacks by filtering OPML files during imports (GHSA-83jp-rww3-57rr)
|
||||||
|
|
||||||
## [7.1.0]
|
## [7.1.0]
|
||||||
|
|
||||||
- Display the unread count in the header on mobile, since the tree is hidden by default and the unread count is not visible otherwise (#2055)
|
- Display the unread count in the header on mobile, since the tree is hidden by default and the unread count is not
|
||||||
|
visible otherwise (#2055)
|
||||||
- The feed refresh engine is now a little bit more performant (#2089)
|
- The feed refresh engine is now a little bit more performant (#2089)
|
||||||
- On shutdown, give the feed refresh engine some time to finish refreshing feeds before killing it. This is controlled by the new `commafeed.shutdown-timeout` setting, which defaults to 2 seconds
|
- On shutdown, give the feed refresh engine some time to finish refreshing feeds before killing it. This is controlled
|
||||||
|
by the new `commafeed.shutdown-timeout` setting, which defaults to 2 seconds
|
||||||
|
|
||||||
## [7.0.0]
|
## [7.0.0]
|
||||||
|
|
||||||
- Replaced the JEXL filter expression for marking feed entries as read automatically with a user-friendly visual query builder. Expressions are now evaluated with Common Expression Language, which is safer than JEXL and sanboxed by default.
|
- Replaced the JEXL filter expression for marking feed entries as read automatically with a user-friendly visual query
|
||||||
- Added a per-feed setting for sending push notifications to ntfy, Gotify or Pushover when new feed entries are discovered (#1610)
|
builder. Expressions are now evaluated with Common Expression Language, which is safer than JEXL and sanboxed by
|
||||||
|
default.
|
||||||
|
- Added a per-feed setting for sending push notifications to ntfy, Gotify or Pushover when new feed entries are
|
||||||
|
discovered (#1610)
|
||||||
- Added a per-feed setting for marking entries as read after a number of days (#2041)
|
- Added a per-feed setting for marking entries as read after a number of days (#2041)
|
||||||
- The default value of `commafeed.http-client.block-local-addresses` is now false, allowing users to subscribe to feeds only available on their local network. This may be a security risk (SSRF) if your instance is accessible by untrusted users, so you may want to set it to true if you host a public instance of CommaFeed with user registeration enabled.
|
- The default value of `commafeed.http-client.block-local-addresses` is now false, allowing users to subscribe to feeds
|
||||||
- When `commafeed.http-client.block-local-addresses` is enabled, SSRF is now also mitigated by blocking public websites redirecting to local ones.
|
only available on their local network. This may be a security risk (SSRF) if your instance is accessible by untrusted
|
||||||
|
users, so you may want to set it to true if you host a public instance of CommaFeed with user registeration enabled.
|
||||||
|
- When `commafeed.http-client.block-local-addresses` is enabled, SSRF is now also mitigated by blocking public websites
|
||||||
|
redirecting to local ones.
|
||||||
|
|
||||||
## [6.2.0]
|
## [6.2.0]
|
||||||
|
|
||||||
- Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new `commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want to keep deleting starred entries during normal entries cleanup (#1581)
|
- Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new
|
||||||
|
`commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want
|
||||||
|
to keep deleting starred entries during normal entries cleanup (#1581)
|
||||||
|
|
||||||
## [6.1.1]
|
## [6.1.1]
|
||||||
|
|
||||||
@@ -40,16 +71,21 @@
|
|||||||
|
|
||||||
## [6.1.0]
|
## [6.1.0]
|
||||||
|
|
||||||
- When clicking on the password reset link, a random password is no longer generated automatically. The user is now redirected to a page where they can set their own password (#2023)
|
- When clicking on the password reset link, a random password is no longer generated automatically. The user is now
|
||||||
|
redirected to a page where they can set their own password (#2023)
|
||||||
- Use browser preferred language instead of English when using CommaFeed for the first time (#2018)
|
- Use browser preferred language instead of English when using CommaFeed for the first time (#2018)
|
||||||
- The profile menu is now closed when scrolling the page (#2019)
|
- The profile menu is now closed when scrolling the page (#2019)
|
||||||
- The "disable pull to refresh" feature is now disabled by default (#2030)
|
- The "disable pull to refresh" feature is now disabled by default (#2030)
|
||||||
|
|
||||||
## [6.0.0]
|
## [6.0.0]
|
||||||
|
|
||||||
- When booting CommaFeed for the first time, the default "admin" account is no longer created automatically. A setup wizard will guide you through the creation of an admin account
|
- When booting CommaFeed for the first time, the default "admin" account is no longer created automatically. A setup
|
||||||
- Default password complexity requirements have been lowered for local network deployments, where strict password rules are often unnecessary. The `commafeed.users.strict-password-policy` setting has been replaced by `commafeed.users.minimum-password-length` with a default value of `4` (#1916)
|
wizard will guide you through the creation of an admin account
|
||||||
- Email addresses are no longer required when creating users and when they update their profile. The `commafeed.users.email-address-required` setting has been added to restore the previous behavior (#1914)
|
- Default password complexity requirements have been lowered for local network deployments, where strict password rules
|
||||||
|
are often unnecessary. The `commafeed.users.strict-password-policy` setting has been replaced by
|
||||||
|
`commafeed.users.minimum-password-length` with a default value of `4` (#1916)
|
||||||
|
- Email addresses are no longer required when creating users and when they update their profile. The
|
||||||
|
`commafeed.users.email-address-required` setting has been added to restore the previous behavior (#1914)
|
||||||
- Java 25+ is now required to build and run CommaFeed
|
- Java 25+ is now required to build and run CommaFeed
|
||||||
|
|
||||||
## [5.12.1]
|
## [5.12.1]
|
||||||
@@ -79,15 +115,18 @@
|
|||||||
|
|
||||||
## [5.10.0]
|
## [5.10.0]
|
||||||
|
|
||||||
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is open (#1762)
|
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is
|
||||||
|
open (#1762)
|
||||||
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
|
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
|
||||||
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
|
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
|
||||||
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087)
|
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are
|
||||||
|
displayed (#1087)
|
||||||
|
|
||||||
## [5.9.0]
|
## [5.9.0]
|
||||||
|
|
||||||
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
|
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
|
||||||
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the new CSS classes
|
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the
|
||||||
|
new CSS classes
|
||||||
- Static resources are now cached for much longer (#1782)
|
- Static resources are now cached for much longer (#1782)
|
||||||
|
|
||||||
## [5.8.0]
|
## [5.8.0]
|
||||||
@@ -96,7 +135,8 @@
|
|||||||
- A font size slider is now available to change the size of the text of feed entries (#1462)
|
- A font size slider is now available to change the size of the text of feed entries (#1462)
|
||||||
- The "mark all as read" confirmation setting now also applies to the "shift+a" keyboard shortcut (#1744)
|
- The "mark all as read" confirmation setting now also applies to the "shift+a" keyboard shortcut (#1744)
|
||||||
- CommaFeed wil try to match the language of the browser before defaulting to english (#1767)
|
- CommaFeed wil try to match the language of the browser before defaulting to english (#1767)
|
||||||
- The default value for the number of entries to keep above the selected entry when scrolling is now 1 instead of 0 to match what other feed readers do
|
- The default value for the number of entries to keep above the selected entry when scrolling is now 1 instead of 0 to
|
||||||
|
match what other feed readers do
|
||||||
|
|
||||||
## [5.7.0]
|
## [5.7.0]
|
||||||
|
|
||||||
@@ -115,14 +155,22 @@
|
|||||||
|
|
||||||
## [5.6.0]
|
## [5.6.0]
|
||||||
|
|
||||||
- To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is now true. This means feeds no longer refresh exactly every 5 minutes (the default value of `commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new `commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active feeds refresh more often (#1677)
|
- To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is
|
||||||
|
now true. This means feeds no longer refresh exactly every 5 minutes (the default value of
|
||||||
|
`commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new
|
||||||
|
`commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active
|
||||||
|
feeds refresh more often (#1677)
|
||||||
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
|
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
|
||||||
- Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses` setting if you subscribe to feeds only available on your local network and you trust all your users
|
- Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could
|
||||||
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does not contain a "Retry-After" header
|
potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses`
|
||||||
|
setting if you subscribe to feeds only available on your local network and you trust all your users
|
||||||
|
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does
|
||||||
|
not contain a "Retry-After" header
|
||||||
|
|
||||||
## [5.5.0]
|
## [5.5.0]
|
||||||
|
|
||||||
- CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this header
|
- CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this
|
||||||
|
header
|
||||||
- Audio enclosures (e.g. podcasts) now fill available entry width
|
- Audio enclosures (e.g. podcasts) now fill available entry width
|
||||||
- Fix an issue with some labels not correctly internationalized
|
- Fix an issue with some labels not correctly internationalized
|
||||||
|
|
||||||
@@ -139,8 +187,10 @@
|
|||||||
## [5.3.5]
|
## [5.3.5]
|
||||||
|
|
||||||
- Fixed an issue with the aspect ratio of images of some feeds (#1595)
|
- Fixed an issue with the aspect ratio of images of some feeds (#1595)
|
||||||
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age property (#1615)
|
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age
|
||||||
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may need to update it to the most recent patch version to support `-proc:full` (#1618)
|
property (#1615)
|
||||||
|
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may
|
||||||
|
need to update it to the most recent patch version to support `-proc:full` (#1618)
|
||||||
|
|
||||||
## [5.3.4]
|
## [5.3.4]
|
||||||
|
|
||||||
@@ -167,19 +217,22 @@
|
|||||||
|
|
||||||
- Added an option to keep a number of entries above the selected entry when scrolling
|
- Added an option to keep a number of entries above the selected entry when scrolling
|
||||||
- Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
|
- Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
|
||||||
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up (every hour)
|
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up
|
||||||
|
(every hour)
|
||||||
- Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
|
- Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
|
||||||
|
|
||||||
## [5.1.1]
|
## [5.1.1]
|
||||||
|
|
||||||
- Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
|
- Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
|
||||||
- When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is selected (#1543)
|
- When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is
|
||||||
|
selected (#1543)
|
||||||
|
|
||||||
## [5.1.0]
|
## [5.1.0]
|
||||||
|
|
||||||
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
|
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
|
||||||
- Fixed an issue that could prevent the app from starting on some systems (#1532)
|
- Fixed an issue that could prevent the app from starting on some systems (#1532)
|
||||||
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date
|
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to
|
||||||
|
date
|
||||||
- Reduced database cleanup log verbosity
|
- Reduced database cleanup log verbosity
|
||||||
|
|
||||||
## [5.0.2]
|
## [5.0.2]
|
||||||
@@ -194,9 +247,9 @@
|
|||||||
## [5.0.0]
|
## [5.0.0]
|
||||||
|
|
||||||
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
||||||
the [announcement](https://github.com/Athou/commafeed/discussions/1517).
|
the [announcement](https://github.com/Athou/commafeed/discussions/1517). The gist of it is that CommaFeed can now be
|
||||||
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
|
compiled to a native binary, resulting in blazing fast startup times (around 0.3s) and very low memory footprint (<
|
||||||
0.3s) and very low memory footprint (< 50M).
|
50M).
|
||||||
|
|
||||||
- CommaFeed now has a different package for each supported database.
|
- CommaFeed now has a different package for each supported database.
|
||||||
- If you are deploying CommaFeed with a precompiled package, please
|
- If you are deploying CommaFeed with a precompiled package, please
|
||||||
@@ -252,8 +305,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
|
|||||||
- add a button in the entry headers to open links in a new tab (#1333)
|
- add a button in the entry headers to open links in a new tab (#1333)
|
||||||
- add two options in the settings to toggle those buttons
|
- add two options in the settings to toggle those buttons
|
||||||
- accept .opml file extension when importing and export with the .opml extension
|
- accept .opml file extension when importing and export with the .opml extension
|
||||||
- the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read (
|
- the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read
|
||||||
older than `keepStatusDays`) (#1303)
|
(older than `keepStatusDays`) (#1303)
|
||||||
|
|
||||||
## [4.3.3]
|
## [4.3.3]
|
||||||
|
|
||||||
@@ -297,9 +350,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
|
|||||||
- it is now possible to open the sidebar on mobile by swiping to the right (#1098)
|
- it is now possible to open the sidebar on mobile by swiping to the right (#1098)
|
||||||
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
|
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
|
||||||
- the full hierarchy of categories are now displayed in the category dropdown (#1045)
|
- the full hierarchy of categories are now displayed in the category dropdown (#1045)
|
||||||
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup.
|
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup. The setting is
|
||||||
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set
|
disabled by default for existing installations, except for the docker image where it is enabled and set to 365 days
|
||||||
to 365 days
|
|
||||||
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
|
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
|
||||||
page instead of the welcome page when not logged in (#1185)
|
page instead of the welcome page when not logged in (#1185)
|
||||||
- the sidebar resizer is no longer shown in the middle of the screen on mobile
|
- the sidebar resizer is no longer shown in the middle of the screen on mobile
|
||||||
@@ -325,8 +377,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
|
|||||||
request, reducing CPU usage
|
request, reducing CPU usage
|
||||||
- updated UI library Mantine to 7.0, improving performance
|
- updated UI library Mantine to 7.0, improving performance
|
||||||
- the h2 embedded database is now compacted on shutdown to reclaim unused space
|
- the h2 embedded database is now compacted on shutdown to reclaim unused space
|
||||||
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is
|
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is recommended
|
||||||
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
|
(see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
|
||||||
- migrated documentation from swagger 2 to openapi 3
|
- migrated documentation from swagger 2 to openapi 3
|
||||||
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
|
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
|
||||||
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
|
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
|
||||||
@@ -568,7 +620,6 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
|
|||||||
## [2.0.0]
|
## [2.0.0]
|
||||||
|
|
||||||
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
|
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
|
||||||
consumption and better overall performances.
|
consumption and better overall performances. See the README on how to build CommaFeed from now on.
|
||||||
See the README on how to build CommaFeed from now on.
|
|
||||||
- CommaFeed should no longer fetch the same feed multiple times in a row
|
- CommaFeed should no longer fetch the same feed multiple times in a row
|
||||||
- Users can use their username or email to log in
|
- Users can use their username or email to log in
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -15,7 +15,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
|
|||||||
- Supports thousands of users and millions of feeds
|
- Supports thousands of users and millions of feeds
|
||||||
- OPML import/export
|
- OPML import/export
|
||||||
- REST API
|
- REST API
|
||||||
- Fever-compatible API for native mobile apps
|
- Fever and Google Reader API for native mobile apps
|
||||||
- Can automatically mark articles as read based on user-defined rules
|
- Can automatically mark articles as read based on user-defined rules
|
||||||
- Push notifications when new articles are published
|
- Push notifications when new articles are published
|
||||||
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
|
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
|
||||||
@@ -159,6 +159,14 @@ named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/download
|
|||||||
This is the JVM used in
|
This is the JVM used in
|
||||||
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
|
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Getting "Access to local address blocked" when adding a feed
|
||||||
|
|
||||||
|
CommaFeed blocks access to local resources by default to prevent [SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
|
||||||
|
If you want to subscribe to feeds that are only available on your local network, you can disable this security measure by setting the `commafeed.http-client.block-local-addresses` variable to `false`.
|
||||||
|
Do this only if you trust all users of your CommaFeed instance not to access private resources.
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
Files for internationalization are
|
Files for internationalization are
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 4,
|
"indentWidth": 4,
|
||||||
|
|||||||
1096
commafeed-client/package-lock.json
generated
1096
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,19 +19,19 @@
|
|||||||
"@fontsource/open-sans": "5.3.0",
|
"@fontsource/open-sans": "5.3.0",
|
||||||
"@lingui/core": "6.6.0",
|
"@lingui/core": "6.6.0",
|
||||||
"@lingui/react": "6.6.0",
|
"@lingui/react": "6.6.0",
|
||||||
"@mantine/core": "9.5.0",
|
"@mantine/core": "9.5.1",
|
||||||
"@mantine/dates": "9.5.0",
|
"@mantine/dates": "9.5.1",
|
||||||
"@mantine/form": "9.5.0",
|
"@mantine/form": "9.5.1",
|
||||||
"@mantine/hooks": "9.5.0",
|
"@mantine/hooks": "9.5.1",
|
||||||
"@mantine/modals": "9.5.0",
|
"@mantine/modals": "9.5.1",
|
||||||
"@mantine/notifications": "9.5.0",
|
"@mantine/notifications": "9.5.1",
|
||||||
"@mantine/spotlight": "9.5.0",
|
"@mantine/spotlight": "9.5.1",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@react-querybuilder/mantine": "8.21.2",
|
"@react-querybuilder/mantine": "8.23.0",
|
||||||
"@reduxjs/toolkit": "2.12.0",
|
"@reduxjs/toolkit": "2.12.0",
|
||||||
"@rolldown/plugin-babel": "0.2.3",
|
"@rolldown/plugin-babel": "0.2.3",
|
||||||
"axios": "1.19.0",
|
"axios": "1.19.0",
|
||||||
"dayjs": "1.11.21",
|
"dayjs": "1.11.23",
|
||||||
"escape-string-regexp": "5.0.0",
|
"escape-string-regexp": "5.0.0",
|
||||||
"interweave": "13.1.1",
|
"interweave": "13.1.1",
|
||||||
"monaco-editor": "0.56.0",
|
"monaco-editor": "0.56.0",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"react-draggable": "4.7.1",
|
"react-draggable": "4.7.1",
|
||||||
"react-icons": "5.7.0",
|
"react-icons": "5.7.0",
|
||||||
"react-infinite-scroller": "1.2.6",
|
"react-infinite-scroller": "1.2.6",
|
||||||
"react-querybuilder": "8.21.2",
|
"react-querybuilder": "8.23.0",
|
||||||
"react-redux": "9.3.0",
|
"react-redux": "9.3.0",
|
||||||
"react-router-dom": "7.18.2",
|
"react-router-dom": "7.18.2",
|
||||||
"react-swipeable": "7.0.2",
|
"react-swipeable": "7.0.2",
|
||||||
@@ -54,13 +54,14 @@
|
|||||||
"websocket-heartbeat-js": "1.1.3"
|
"websocket-heartbeat-js": "1.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.6",
|
"@babel/types": "^8.0.4",
|
||||||
|
"@biomejs/biome": "2.5.9",
|
||||||
"@lingui/babel-plugin-lingui-macro": "6.6.0",
|
"@lingui/babel-plugin-lingui-macro": "6.6.0",
|
||||||
"@lingui/cli": "6.6.0",
|
"@lingui/cli": "6.6.0",
|
||||||
"@lingui/vite-plugin": "6.6.0",
|
"@lingui/vite-plugin": "6.6.0",
|
||||||
"@testing-library/jest-dom": "7.0.0",
|
"@testing-library/jest-dom": "7.0.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.5",
|
||||||
"@types/mousetrap": "1.6.15",
|
"@types/mousetrap": "1.6.15",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.4",
|
"@types/react-dom": "19.2.4",
|
||||||
@@ -71,9 +72,9 @@
|
|||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"jsdom": "30.0.1",
|
"jsdom": "30.0.1",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.2.0",
|
"vite": "8.2.1",
|
||||||
"vite-plugin-checker": "0.14.5",
|
"vite-plugin-checker": "0.14.5",
|
||||||
"vitest": "4.1.10",
|
"vitest": "4.1.11",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.2.1</version>
|
<version>7.3.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<name>CommaFeed Client</name>
|
<name>CommaFeed Client</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- renovate: datasource=node-version depName=node -->
|
<!-- renovate: datasource=node-version depName=node -->
|
||||||
<node.version>v24.18.1</node.version>
|
<node.version>v24.19.0</node.version>
|
||||||
<!-- renovate: datasource=npm depName=npm -->
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
<npm.version>12.0.2</npm.version>
|
<npm.version>12.0.2</npm.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|||||||
36
commafeed-client/src/app/client.test.ts
Normal file
36
commafeed-client/src/app/client.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { i18n } from "@lingui/core"
|
||||||
|
import type { AxiosError } from "axios"
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||||
|
import { errorToStrings } from "./client"
|
||||||
|
|
||||||
|
const axiosError = (status: number, data: unknown) =>
|
||||||
|
({
|
||||||
|
isAxiosError: true,
|
||||||
|
response: { status, data },
|
||||||
|
}) as AxiosError
|
||||||
|
|
||||||
|
describe("errorToStrings", () => {
|
||||||
|
afterEach(() => vi.restoreAllMocks())
|
||||||
|
|
||||||
|
it("translates known application error types", () => {
|
||||||
|
vi.spyOn(i18n, "_").mockReturnValue("Translated authentication error")
|
||||||
|
const error = axiosError(401, {
|
||||||
|
type: "WRONG_USERNAME_OR_PASSWORD",
|
||||||
|
message: "wrong username or password",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errorToStrings(error)).toEqual(["Translated authentication error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves backend messages for unexpected errors", () => {
|
||||||
|
const error = axiosError(500, { message: "unexpected error" })
|
||||||
|
|
||||||
|
expect(errorToStrings(error)).toEqual(["unexpected error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves backend messages for unknown application error types", () => {
|
||||||
|
const error = axiosError(400, { type: "UNKNOWN_ERROR", message: "unknown error" })
|
||||||
|
|
||||||
|
expect(errorToStrings(error)).toEqual(["unknown error"])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { i18n, type MessageDescriptor } from "@lingui/core"
|
||||||
|
import { msg } from "@lingui/core/macro"
|
||||||
import axios, { type AxiosError } from "axios"
|
import axios, { type AxiosError } from "axios"
|
||||||
import type {
|
import type {
|
||||||
AddCategoryRequest,
|
AddCategoryRequest,
|
||||||
@@ -6,6 +8,8 @@ import type {
|
|||||||
Category,
|
Category,
|
||||||
CategoryModificationRequest,
|
CategoryModificationRequest,
|
||||||
CollapseRequest,
|
CollapseRequest,
|
||||||
|
CommaFeedApplicationError,
|
||||||
|
CommaFeedExceptionType,
|
||||||
Entries,
|
Entries,
|
||||||
FeedInfo,
|
FeedInfo,
|
||||||
FeedInfoRequest,
|
FeedInfoRequest,
|
||||||
@@ -31,6 +35,10 @@ import type {
|
|||||||
UserModel,
|
UserModel,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
|
const applicationErrorMessages = {
|
||||||
|
WRONG_USERNAME_OR_PASSWORD: msg`Wrong username or password`,
|
||||||
|
} satisfies Record<CommaFeedExceptionType, MessageDescriptor>
|
||||||
|
|
||||||
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
response => response,
|
response => response,
|
||||||
@@ -128,14 +136,25 @@ export const errorToStrings = (err: unknown) => {
|
|||||||
let strings: string[] = []
|
let strings: string[] = []
|
||||||
|
|
||||||
if (axios.isAxiosError(err) && err.response) {
|
if (axios.isAxiosError(err) && err.response) {
|
||||||
if (typeof err.response.data === "string") strings.push(err.response.data)
|
if (isCommaFeedApplicationError(err)) {
|
||||||
if (isMessageError(err)) strings.push(err.response.data.message)
|
strings.push(i18n._(applicationErrorMessages[err.response.data.type]))
|
||||||
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
|
} else {
|
||||||
|
if (typeof err.response.data === "string") strings.push(err.response.data)
|
||||||
|
if (isMessageError(err)) strings.push(err.response.data.message)
|
||||||
|
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings
|
return strings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCommaFeedApplicationError(err: AxiosError): err is AxiosError<CommaFeedApplicationError> {
|
||||||
|
const data = err.response?.data
|
||||||
|
if (!data || typeof data !== "object" || !("type" in data)) return false
|
||||||
|
const type = data.type
|
||||||
|
return typeof type === "string" && Object.hasOwn(applicationErrorMessages, type)
|
||||||
|
}
|
||||||
|
|
||||||
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
|
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
|
||||||
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
|
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ export const starEntry = createAppAsyncThunk(
|
|||||||
(arg: { entry: Entry; starred: boolean }) => {
|
(arg: { entry: Entry; starred: boolean }) => {
|
||||||
client.entry.star({
|
client.entry.star({
|
||||||
id: arg.entry.id,
|
id: arg.entry.id,
|
||||||
feedId: +arg.entry.feedId,
|
|
||||||
starred: arg.starred,
|
starred: arg.starred,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ export interface LocalSettings {
|
|||||||
|
|
||||||
export interface StarRequest {
|
export interface StarRequest {
|
||||||
id: string
|
id: string
|
||||||
feedId: number
|
|
||||||
starred: boolean
|
starred: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,3 +337,10 @@ export interface AuthenticationError {
|
|||||||
message: string
|
message: string
|
||||||
allowRegistrations: boolean
|
allowRegistrations: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CommaFeedExceptionType = "WRONG_USERNAME_OR_PASSWORD"
|
||||||
|
|
||||||
|
export interface CommaFeedApplicationError {
|
||||||
|
type: CommaFeedExceptionType
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
|
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened} preventPositionChangeWhenVisible={false}>
|
||||||
<Menu.Target>{props.control}</Menu.Target>
|
<Menu.Target>{props.control}</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{profile && <Menu.Label>{profile.name}</Menu.Label>}
|
{profile && <Menu.Label>{profile.name}</Menu.Label>}
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ export function ProfileSettings() {
|
|||||||
label={<Trans>API key</Trans>}
|
label={<Trans>API key</Trans>}
|
||||||
description={
|
description={
|
||||||
<Trans>
|
<Trans>
|
||||||
This is your API key. It can be used for some read-only API operations and grants access to the Fever API.
|
This is your API key. It can be used for some read-only API operations and grants access to the Fever API
|
||||||
Use the form at the bottom of the page to generate a new API key
|
and the Google Reader API. Use the form at the bottom of the page to generate a new API key
|
||||||
</Trans>
|
</Trans>
|
||||||
}
|
}
|
||||||
readOnly
|
readOnly
|
||||||
@@ -130,6 +130,22 @@ export function ProfileSettings() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Input.Wrapper>
|
</Input.Wrapper>
|
||||||
|
|
||||||
|
<Input.Wrapper
|
||||||
|
label={<Trans>Google Reader API</Trans>}
|
||||||
|
description={
|
||||||
|
<Trans>
|
||||||
|
CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible
|
||||||
|
mobile client. Login with your username and your <u>API key</u>.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Anchor href="rest/googlereader" target="_blank">
|
||||||
|
<Trans>Google Reader API URL</Trans>
|
||||||
|
</Anchor>
|
||||||
|
</Box>
|
||||||
|
</Input.Wrapper>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "إصدار إضافة متصفح CommaFeed هو {browserExtensionVersion
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed متوافق مع Fever API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Fever. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك."
|
msgstr "CommaFeed متوافق مع Fever API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Fever. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed متوافق مع Google Reader API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Google Reader. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed التالي العنصر غير المقروء"
|
msgstr "CommaFeed التالي العنصر غير المقروء"
|
||||||
@@ -454,6 +458,14 @@ msgstr "انتقل إلى وثائق API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "الأشياء الجيدة"
|
msgstr "الأشياء الجيدة"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "عنبي"
|
msgstr "عنبي"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "تحتوي هذه الخلاصة على فلتر قديم لا يمكن تحريره ولا يتم تطبيقه. يرجى إعادة إنشاء الفلتر باستخدام محرر التعبيرات الجديد. كان تعبير الفلتر القديم هو: <0>{0}</0>"
|
msgstr "تحتوي هذه الخلاصة على فلتر قديم لا يمكن تحريره ولا يتم تطبيقه. يرجى إعادة إنشاء الفلتر باستخدام محرر التعبيرات الجديد. كان تعبير الفلتر القديم هو: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح الوصول إلى Fever API. استخدم النموذج الموجود في أسفل الصفحة لإنشاء مفتاح API جديد"
|
msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح حق الوصول إلى Fever API و Google Reader API. استخدم النموذج الموجود أسفل الصفحة لإنشاء مفتاح API جديد"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "موقع الكتروني"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "مرحباً! يبدو أن هذه هي المرة الأولى التي تقوم فيها بتشغيل CommaFeed. يرجى إنشاء حساب مسؤول للبدء."
|
msgstr "مرحباً! يبدو أن هذه هي المرة الأولى التي تقوم فيها بتشغيل CommaFeed. يرجى إنشاء حساب مسؤول للبدء."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "أصفر"
|
msgstr "أصفر"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
|
msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed és compatible amb l'API de Google Reader. Utilitzeu l'URL següent al vostre client mòbil compatible amb Google Reader. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed següent element no llegit"
|
msgstr "CommaFeed següent element no llegit"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Vés a la documentació de l'API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Extres"
|
msgstr "Extres"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Raïm"
|
msgstr "Raïm"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Aquest feed té un filtre heretat que no es pot editar i no s'aplica. Torneu a crear el filtre amb el nou editor d'expressions. L'expressió del filtre heretat era: <0>{0}</0>"
|
msgstr "Aquest feed té un filtre heretat que no es pot editar i no s'aplica. Torneu a crear el filtre amb el nou editor d'expressions. L'expressió del filtre heretat era: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Aquesta és la vostra clau de l'API. Es pot utilitzar per a algunes operacions de l'API de només lectura i permet accedir a l'API Fever. Utilitzeu el formulari de la part inferior de la pàgina per generar una nova clau d'API."
|
msgstr "Aquesta és la vostra clau API. Es pot utilitzar per a algunes operacions de l'API de només lectura i dona accés a l'API Fever i a l'API de Google Reader. Utilitzeu el formulari a la part inferior de la pàgina per generar una nova clau API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Lloc web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Benvingut! Sembla que aquesta és la primera vegada que executeu CommaFeed. Creeu un compte d'administrador per començar."
|
msgstr "Benvingut! Sembla que aquesta és la primera vegada que executeu CommaFeed. Creeu un compte d'administrador per començar."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Groc"
|
msgstr "Groc"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Verze rozšíření prohlížeče CommaFeed {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed je kompatibilní s Fever API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Fever. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>."
|
msgstr "CommaFeed je kompatibilní s Fever API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Fever. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed je kompatibilní s Google Reader API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Google Reader. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed další nepřečtená položka"
|
msgstr "CommaFeed další nepřečtená položka"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Přejděte na dokumentaci API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Dobroty"
|
msgstr "Dobroty"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Hroznová"
|
msgstr "Hroznová"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Tento zdroj má zastaralý filtr, který nelze upravovat a není aplikován. Znovu vytvořte filtr pomocí nového editoru výrazů. Původní výraz filtru byl: <0>{0}</0>"
|
msgstr "Tento zdroj má zastaralý filtr, který nelze upravovat a není aplikován. Znovu vytvořte filtr pomocí nového editoru výrazů. Původní výraz filtru byl: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a poskytuje přístup k Fever API. Pro vygenerování nového klíče API použijte formulář v dolní části stránky."
|
msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a uděluje přístup k Fever API a Google Reader API. Použijte formulář v dolní části stránky k vygenerování nového klíče API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Webová stránka"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Vítejte! Zdá se, že je to poprvé, co spouštíte CommaFeed. Chcete-li začít, vytvořte si účet správce."
|
msgstr "Vítejte! Zdá se, že je to poprvé, co spouštíte CommaFeed. Chcete-li začít, vytvořte si účet správce."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Žlutá"
|
msgstr "Žlutá"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Fersiwn estyniad porwr CommaFeed {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "Mae CommaFeed yn gydnaws â'r Fever API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Fever. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>."
|
msgstr "Mae CommaFeed yn gydnaws â'r Fever API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Fever. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "Mae CommaFeed yn gydnaws â Google Reader API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Google Reader. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed eitem nesaf heb ei darllen"
|
msgstr "CommaFeed eitem nesaf heb ei darllen"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Ewch i'r ddogfennaeth API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "nwyddau"
|
msgstr "nwyddau"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Grawnwin"
|
msgstr "Grawnwin"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Mae gan y porthiant hwn hen hidlydd na ellir ei olygu ac nid yw'n cael ei gymhwyso. Crëwch yr hidlydd eto gan ddefnyddio'r golygydd mynegiant newydd. Yr hen fynegiant hidlo oedd: <0>{0}</0>"
|
msgstr "Mae gan y porthiant hwn hen hidlydd na ellir ei olygu ac nid yw'n cael ei gymhwyso. Crëwch yr hidlydd eto gan ddefnyddio'r golygydd mynegiant newydd. Yr hen fynegiant hidlo oedd: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dyma'ch allwedd API. Gellir ei ddefnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n caniatáu mynediad i'r Fever API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd"
|
msgstr "Dyma eich allwedd API. Gellir ei defnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n rhoi mynediad i'r Fever API a'r Google Reader API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Gwefan"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Croeso! Ymddengys mai dyma'r tro cyntaf i chi redeg CommaFeed. Creuwch gyfrif gweinyddwr i ddechrau."
|
msgstr "Croeso! Ymddengys mai dyma'r tro cyntaf i chi redeg CommaFeed. Creuwch gyfrif gweinyddwr i ddechrau."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Melyn"
|
msgstr "Melyn"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed browserudvidelse version {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed er kompatibel med Fever API. Brug følgende URL i din Fever-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>."
|
msgstr "CommaFeed er kompatibel med Fever API. Brug følgende URL i din Fever-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed er kompatibel med Google Reader API. Brug følgende URL i din Google Reader-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed næste ulæste element"
|
msgstr "CommaFeed næste ulæste element"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentationen."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Godbidder"
|
msgstr "Godbidder"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Grape"
|
msgstr "Grape"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Dette feed har et ældre filter, der ikke kan redigeres og ikke anvendes. Genopret venligst filteret ved hjælp af den nye udtrykseditor. Det ældre filterudtryk var: <0>{0}</0>"
|
msgstr "Dette feed har et ældre filter, der ikke kan redigeres og ikke anvendes. Genopret venligst filteret ved hjælp af den nye udtrykseditor. Det ældre filterudtryk var: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dette er din API-nøgle. Den kan bruges til visse skrivebeskyttede API-operationer og giver adgang til Fever API. Brug formularen nederst på siden til at generere en ny API-nøgle"
|
msgstr "Dette er din API-nøgle. Den kan bruges til nogle skrivebeskyttede API-operationer og giver adgang til Fever API og Google Reader API. Brug formularen nederst på siden for at generere en ny API-nøgle"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Hjemmeside"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Velkommen! Det ser ud til at være første gang, du kører CommaFeed. Opret venligst en administratorkonto for at komme i gang."
|
msgstr "Velkommen! Det ser ud til at være første gang, du kører CommaFeed. Opret venligst en administratorkonto for at komme i gang."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gul"
|
msgstr "Gul"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
|
msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed ist mit der Google-Reader-API kompatibel. Verwenden Sie die folgende URL in Ihrem Google-Reader-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed nächstes ungelesenes Element"
|
msgstr "CommaFeed nächstes ungelesenes Element"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Gehen Sie zur API-Dokumentation."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Goodies"
|
msgstr "Goodies"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Traube"
|
msgstr "Traube"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Dieser Feed verfügt über einen veralteten Filter, der nicht bearbeitet werden kann und nicht angewendet wird. Bitte erstellen Sie den Filter mit dem neuen Ausdruckseditor neu. Der veraltete Filterausdruck war: <0>{0}</0>"
|
msgstr "Dieser Feed verfügt über einen veralteten Filter, der nicht bearbeitet werden kann und nicht angewendet wird. Bitte erstellen Sie den Filter mit dem neuen Ausdruckseditor neu. Der veraltete Filterausdruck war: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und ermöglicht den Zugriff auf die Fever-API. Verwenden Sie das Formular unten auf der Seite, um einen neuen API-Schlüssel zu generieren"
|
msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und gewährt Zugriff auf die Fever-API und die Google-Reader-API. Verwenden Sie das Formular am Ende der Seite, um einen neuen API-Schlüssel zu generieren"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Webseite"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Willkommen! Dies scheint das erste Mal zu sein, dass Sie CommaFeed ausführen. Bitte erstellen Sie ein Administrator-Konto, um zu beginnen."
|
msgstr "Willkommen! Dies scheint das erste Mal zu sein, dass Sie CommaFeed ausführen. Bitte erstellen Sie ein Administrator-Konto, um zu beginnen."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gelb"
|
msgstr "Gelb"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed next unread item"
|
msgstr "CommaFeed next unread item"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Go to the API documentation."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Goodies"
|
msgstr "Goodies"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr "Google Reader API"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr "Google Reader API URL"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Grape"
|
msgstr "Grape"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
msgstr "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Website"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgstr "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr "Wrong username or password"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Yellow"
|
msgstr "Yellow"
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ msgstr "Versión de la extensión del navegador CommaFeed {browserExtensionVersi
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
|
msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed es compatible con Google Reader API. Utilice la siguiente URL en su cliente móvil compatible con Google Reader. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed siguiente elemento no leído"
|
msgstr "CommaFeed siguiente elemento no leído"
|
||||||
@@ -455,6 +459,14 @@ msgstr "Ir a la documentación de la API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Golosinas"
|
msgstr "Golosinas"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Uva"
|
msgstr "Uva"
|
||||||
@@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Este feed tiene un filtro heredado que no se puede editar y no se aplica. Por favor, vuelve a crear el filtro usando el nuevo editor de expresiones. La expresión del filtro heredado era: <0>{0}</0>"
|
msgstr "Este feed tiene un filtro heredado que no se puede editar y no se aplica. Por favor, vuelve a crear el filtro usando el nuevo editor de expresiones. La expresión del filtro heredado era: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a Fever API. Utilice el formulario en la parte inferior de la página para generar una nueva clave API"
|
msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a la API Fever y a la API de Google Reader. Utilice el formulario en la parte inferior de la página para generar una nueva clave API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1139,6 +1151,10 @@ msgstr "Sitio web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "¡Bienvenido! Parece que esta es la primera vez que ejecutas CommaFeed. Por favor, crea una cuenta de administrador para empezar."
|
msgstr "¡Bienvenido! Parece que esta es la primera vez que ejecutas CommaFeed. Por favor, crea una cuenta de administrador para empezar."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Amarillo"
|
msgstr "Amarillo"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "نسخه افزونه مرورگر CommaFeed {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed با Fever API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Fever استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید."
|
msgstr "CommaFeed با Fever API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Fever استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed با Google Reader API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Google Reader استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "مورد خوانده نشده بعدی CommaFeed"
|
msgstr "مورد خوانده نشده بعدی CommaFeed"
|
||||||
@@ -454,6 +458,14 @@ msgstr "به مستندات API بروید."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "خوبی ها"
|
msgstr "خوبی ها"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "عنبی"
|
msgstr "عنبی"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "این فید دارای یک فیلتر قدیمی است که قابل ویرایش نیست و اعمال نمیشود. لطفاً فیلتر را با استفاده از ویرایشگر جدید بازسازی کنید. عبارت فیلتر قدیمی: <0>{0}</0>"
|
msgstr "این فید دارای یک فیلتر قدیمی است که قابل ویرایش نیست و اعمال نمیشود. لطفاً فیلتر را با استفاده از ویرایشگر جدید بازسازی کنید. عبارت فیلتر قدیمی: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "این کلید API شماست. میتوان از آن برای برخی عملیات API فقط خواندنی استفاده کرد و دسترسی به Fever API را فراهم میکند. از فرم پایین صفحه برای تولید کلید جدید استفاده کنید"
|
msgstr "این کلید API شماست. میتوان از آن برای برخی عملیات فقط خواندنی API استفاده کرد و دسترسی به Fever API و Google Reader API را میدهد. از فرم پایین صفحه برای ایجاد کلید API جدید استفاده کنید"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "وب سایت"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "خوش آمدید! به نظر میرسد این اولین بار است که CommaFeed را اجرا میکنید. لطفاً برای شروع یک حساب مدیر ایجاد کنید."
|
msgstr "خوش آمدید! به نظر میرسد این اولین بار است که CommaFeed را اجرا میکنید. لطفاً برای شروع یک حساب مدیر ایجاد کنید."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "زرد"
|
msgstr "زرد"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed-selainlaajennuksen versio {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed on yhteensopiva Fever-API:n kanssa. Käytä seuraavaa URL-osoitetta Fever-yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>."
|
msgstr "CommaFeed on yhteensopiva Fever-API:n kanssa. Käytä seuraavaa URL-osoitetta Fever-yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed on yhteensopiva Google Reader -API:n kanssa. Käytä seuraavaa URL-osoitetta Google Reader -yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed seuraava lukematon kohde"
|
msgstr "CommaFeed seuraava lukematon kohde"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Siirry API-dokumentaatioon."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Hyvää"
|
msgstr "Hyvää"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Viinirypäle"
|
msgstr "Viinirypäle"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Tässä syötteessä on vanha suodatin, jota ei voi muokata ja jota ei sovelleta. Luo suodatin uudelleen käyttämällä uutta lauseke-editoria. Vanha suodatinlauseke oli: <0>{0}</0>"
|
msgstr "Tässä syötteessä on vanha suodatin, jota ei voi muokata ja jota ei sovelleta. Luo suodatin uudelleen käyttämällä uutta lauseke-editoria. Vanha suodatinlauseke oli: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin ja se antaa pääsyn Fever-API:iin. Käytä sivun alareunassa olevaa lomaketta uuden API-avaimen luomiseen"
|
msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin, ja se myöntää pääsyn Fever-API:hin ja Google Reader -API:hin. Käytä sivun alareunassa olevaa lomaketta luodaksesi uuden API-avaimen"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Verkkosivusto"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Tervetuloa! Näyttää siltä, että suoritat CommaFeediä ensimmäistä kertaa. Luo ylläpitäjän tili aloittaaksesi."
|
msgstr "Tervetuloa! Näyttää siltä, että suoritat CommaFeediä ensimmäistä kertaa. Luo ylläpitäjän tili aloittaaksesi."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Keltainen"
|
msgstr "Keltainen"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
|
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "Commafeed est compatible avec l'API Google Reader, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed prochain article non lu"
|
msgstr "CommaFeed prochain article non lu"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Aller à la documentation de l'API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Extensions"
|
msgstr "Extensions"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr "API Google Reader"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr "URL de l'API Google Reader"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Bordeaux"
|
msgstr "Bordeaux"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Ce flux est configuré avec un filtre obsolète, qui ne peut être ni modifié ni appliqué. Vous devez recréer ce filtre à l'aide du nouvel éditeur d'expressions régulières. Le filtre configuré était : <0>{0}</0>"
|
msgstr "Ce flux est configuré avec un filtre obsolète, qui ne peut être ni modifié ni appliqué. Vous devez recréer ce filtre à l'aide du nouvel éditeur d'expressions régulières. Le filtre configuré était : <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
|
msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever et à l'API Google Reader. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Site web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Bienvenue ! Il semble que ce soit le premier démarrage de Commafeed. Avant tout, vous devez créer un compte administrateur."
|
msgstr "Bienvenue ! Il semble que ce soit le premier démarrage de Commafeed. Avant tout, vous devez créer un compte administrateur."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr "Utilisateur ou mot de passe incorrect"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Jaune"
|
msgstr "Jaune"
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ msgstr "Versión do complemento do navegador para CommaFeed {browserExtensionVer
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed é compatible coa API de Fever. Usa o seguinte URL no teu cliente móbil compatible con Fever. Accede co identificador e a túa <0>clave API</0>."
|
msgstr "CommaFeed é compatible coa API de Fever. Usa o seguinte URL no teu cliente móbil compatible con Fever. Accede co identificador e a túa <0>clave API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed é compatible coa API de Google Reader. Usa o seguinte URL no teu cliente móbil compatible con Google Reader. Accede co identificador e a túa <0>clave API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "Seguinte elemento non lido en CommaFeed"
|
msgstr "Seguinte elemento non lido en CommaFeed"
|
||||||
@@ -455,6 +459,14 @@ msgstr "Ir á documentación da API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Agasallos"
|
msgstr "Agasallos"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Bago"
|
msgstr "Bago"
|
||||||
@@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Esta canle ten un filtro antigo que non se pode editar e que non se aplica. Volve crear o filtro usando o novo editor de expresións. A expresión do filtro antigo era: <0>{0}</0>"
|
msgstr "Esta canle ten un filtro antigo que non se pode editar e que non se aplica. Volve crear o filtro usando o novo editor de expresións. A expresión do filtro antigo era: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever. Usa o formulario da parte inferior da páxina para crear unha nova clave da API"
|
msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever e á API de Google Reader. Usa o formulario da parte inferior da páxina para crear unha nova clave da API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1139,6 +1151,10 @@ msgstr "Páxina web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración."
|
msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Amarelo"
|
msgstr "Amarelo"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed böngészőbővítmény {browserExtensionVersion} verzió."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "A CommaFeed kompatibilis a Fever API-val. Használja a következő URL-t Fever-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>."
|
msgstr "A CommaFeed kompatibilis a Fever API-val. Használja a következő URL-t Fever-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "A CommaFeed kompatibilis a Google Reader API-val. Használja a következő URL-t Google Reader-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed következő olvasatlan elem"
|
msgstr "CommaFeed következő olvasatlan elem"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Nyissa meg az API dokumentációját."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Jók"
|
msgstr "Jók"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Szőlő"
|
msgstr "Szőlő"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Ez a feed egy régi szűrővel rendelkezik, amely nem szerkeszthető és nem kerül alkalmazásra. Kérjük, hozza létre újra a szűrőt az új kifejezésszerkesztővel. A régi szűrőkifejezés a következő volt: <0>{0}</0>"
|
msgstr "Ez a feed egy régi szűrővel rendelkezik, amely nem szerkeszthető és nem kerül alkalmazásra. Kérjük, hozza létre újra a szűrőt az új kifejezésszerkesztővel. A régi szűrőkifejezés a következő volt: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Ez az Ön API-kulcsa. Felhasználható néhány írásvédett API-művelethez, és hozzáférést biztosít a Fever API-hoz. Használja az oldal alján található űrlapot egy új API-kulcs létrehozásához"
|
msgstr "Ez az Ön API-kulcsa. Néhány csak olvasható API-művelethez használható, és hozzáférést biztosít a Fever API-hoz és a Google Reader API-hoz. Használja az oldal alján található űrlapot új API-kulcs létrehozásához"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Webhely"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Üdvözöljük! Úgy tűnik, ez az első alkalom, hogy a CommaFeedet futtatja. Kérjük, hozzon létre egy rendszergazdai fiókot a kezdéshez."
|
msgstr "Üdvözöljük! Úgy tűnik, ez az első alkalom, hogy a CommaFeedet futtatja. Kérjük, hozzon létre egy rendszergazdai fiókot a kezdéshez."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Sárga"
|
msgstr "Sárga"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Ekstensi browser CommaFeed versi {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed kompatibel dengan Fever API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Fever. Login dengan nama pengguna dan <0>kunci API</0> Anda."
|
msgstr "CommaFeed kompatibel dengan Fever API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Fever. Login dengan nama pengguna dan <0>kunci API</0> Anda."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed kompatibel dengan Google Reader API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Google Reader. Login dengan nama pengguna dan <0>kunci API</0> Anda."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed item yang belum dibaca berikutnya"
|
msgstr "CommaFeed item yang belum dibaca berikutnya"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Buka dokumentasi API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Pernak-pernik"
|
msgstr "Pernak-pernik"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Anggur"
|
msgstr "Anggur"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Umpan ini memiliki filter lama yang tidak dapat diedit dan tidak diterapkan. Silakan buat ulang filter menggunakan editor ekspresi baru. Ekspresi filter lama adalah: <0>{0}</0>"
|
msgstr "Umpan ini memiliki filter lama yang tidak dapat diedit dan tidak diterapkan. Silakan buat ulang filter menggunakan editor ekspresi baru. Ekspresi filter lama adalah: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API baca-saja dan memberikan akses ke Fever API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru"
|
msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API yang hanya-baca dan memberikan akses ke Fever API dan Google Reader API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Situs Web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Selamat datang! Ini sepertinya pertama kalinya Anda menjalankan CommaFeed. Silakan buat akun administrator untuk memulai."
|
msgstr "Selamat datang! Ini sepertinya pertama kalinya Anda menjalankan CommaFeed. Silakan buat akun administrator untuk memulai."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Kuning"
|
msgstr "Kuning"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Versione dell'estensione del browser CommaFeed {browserExtensionVersion}
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed è compatibile con l'API Fever. Utilizza il seguente URL nel tuo client mobile compatibile con Fever. Accedi con il tuo nome utente e la tua <0>chiave API</0>."
|
msgstr "CommaFeed è compatibile con l'API Fever. Utilizza il seguente URL nel tuo client mobile compatibile con Fever. Accedi con il tuo nome utente e la tua <0>chiave API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed è compatibile con l'API Google Reader. Utilizza il seguente URL nel tuo client mobile compatibile con Google Reader. Accedi con il tuo nome utente e la tua <0>chiave API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed successivo elemento non letto"
|
msgstr "CommaFeed successivo elemento non letto"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Vai alla documentazione dell'API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Chicche"
|
msgstr "Chicche"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Uva"
|
msgstr "Uva"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Questo feed ha un filtro legacy che non può essere modificato e non viene applicato. Ricrea il filtro utilizzando il nuovo editor di espressioni. L'espressione del filtro legacy era: <0>{0}</0>"
|
msgstr "Questo feed ha un filtro legacy che non può essere modificato e non viene applicato. Ricrea il filtro utilizzando il nuovo editor di espressioni. L'espressione del filtro legacy era: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e garantisce l'accesso all'API Fever. Utilizza il modulo in fondo alla pagina per generare una nuova chiave API"
|
msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e concede l'accesso all'API Fever e all'API Google Reader. Usa il modulo in fondo alla pagina per generare una nuova chiave API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Sito web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Benvenuto! Sembra che sia la prima volta che esegui CommaFeed. Crea un account amministratore per iniziare."
|
msgstr "Benvenuto! Sembra che sia la prima volta che esegui CommaFeed. Crea un account amministratore per iniziare."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Giallo"
|
msgstr "Giallo"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed ブラウザー拡張機能のバージョンは {browserExten
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
|
msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeedはGoogle Reader APIと互換性があります。Google Reader互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed 次の未読アイテム"
|
msgstr "CommaFeed 次の未読アイテム"
|
||||||
@@ -454,6 +458,14 @@ msgstr "APIドキュメントに移動します。"
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "グッズ"
|
msgstr "グッズ"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "グレープ"
|
msgstr "グレープ"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "このフィードには、編集できず適用もされない古い形式のフィルタがあります。新しい式エディタを使用してフィルタを再作成してください。古いフィルタ式は次のとおりでした: <0>{0}</0>"
|
msgstr "このフィードには、編集できず適用もされない古い形式のフィルタがあります。新しい式エディタを使用してフィルタを再作成してください。古いフィルタ式は次のとおりでした: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。"
|
msgstr "これはあなたのAPIキーです。一部の読み取り専用API操作に使用でき、Fever APIおよびGoogle Reader APIへのアクセスを許可します。新しいAPIキーを生成するには、ページ下部のフォームを使用してください"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "ウェブサイト"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "ようこそ!CommaFeedを初めて実行するようです。開始するには管理者アカウントを作成してください。"
|
msgstr "ようこそ!CommaFeedを初めて実行するようです。開始するには管理者アカウントを作成してください。"
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "イエロー"
|
msgstr "イエロー"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed 브라우저 확장 프로그램 버전 {browserExtensionVersi
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed는 Fever API와 호환됩니다. Fever 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오."
|
msgstr "CommaFeed는 Fever API와 호환됩니다. Fever 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed는 Google Reader API와 호환됩니다. Google Reader 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
|
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
|
||||||
@@ -454,6 +458,14 @@ msgstr "API 문서로 이동합니다."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "굿즈"
|
msgstr "굿즈"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "그레이프"
|
msgstr "그레이프"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "이 피드에는 편집할 수 없고 적용되지 않는 기존 필터가 있습니다. 새 표현식 편집기를 사용하여 필터를 다시 만드십시오. 기존 필터 표현식은 <0>{0}</0>이었습니다."
|
msgstr "이 피드에는 편집할 수 없고 적용되지 않는 기존 필터가 있습니다. 새 표현식 편집기를 사용하여 필터를 다시 만드십시오. 기존 필터 표현식은 <0>{0}</0>이었습니다."
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "이것은 귀하의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오."
|
msgstr "이것은 회원님의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API 및 Google Reader API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "웹사이트"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "환영합니다! CommaFeed를 처음 실행하는 것 같습니다. 시작하려면 관리자 계정을 만드십시오."
|
msgstr "환영합니다! CommaFeed를 처음 실행하는 것 같습니다. 시작하려면 관리자 계정을 만드십시오."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "옐로우"
|
msgstr "옐로우"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Ekstensi penyemak imbas CommaFeed versi {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed serasi dengan Fever API. Gunakan URL berikut dalam klien mudah alih serasi Fever anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda."
|
msgstr "CommaFeed serasi dengan Fever API. Gunakan URL berikut dalam klien mudah alih serasi Fever anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed serasi dengan Google Reader API. Gunakan URL berikut dalam klien mudah alih serasi Google Reader anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed item belum dibaca seterusnya"
|
msgstr "CommaFeed item belum dibaca seterusnya"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Pergi ke dokumentasi API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Barangan"
|
msgstr "Barangan"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Anggur"
|
msgstr "Anggur"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Suapan ini mempunyai penapis warisan yang tidak boleh diedit dan tidak digunakan. Sila buat semula penapis menggunakan editor ungkapan baharu. Ungkapan penapis warisan ialah: <0>{0}</0>"
|
msgstr "Suapan ini mempunyai penapis warisan yang tidak boleh diedit dan tidak digunakan. Sila buat semula penapis menggunakan editor ungkapan baharu. Ungkapan penapis warisan ialah: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk beberapa operasi API baca sahaja dan memberikan akses kepada Fever API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu"
|
msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk sesetengah operasi API baca sahaja dan memberikan akses kepada Fever API dan Google Reader API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Laman web"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Selamat datang! Ini nampaknya kali pertama anda menjalankan CommaFeed. Sila cipta akaun pentadbir untuk bermula."
|
msgstr "Selamat datang! Ini nampaknya kali pertama anda menjalankan CommaFeed. Sila cipta akaun pentadbir untuk bermula."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Kuning"
|
msgstr "Kuning"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed nettleserutvidelse versjon {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgende URL i din Fever-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>."
|
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgende URL i din Fever-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgende URL i din Google Reader-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed neste uleste element"
|
msgstr "CommaFeed neste uleste element"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Godbiter"
|
msgstr "Godbiter"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Drue"
|
msgstr "Drue"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Denne feeden har et utdatert filter som ikke kan redigeres og ikke blir brukt. Vennligst opprett filteret på nytt med den nye uttrykkseditoren. Det utdaterte filteruttrykket var: <0>{0}</0>"
|
msgstr "Denne feeden har et utdatert filter som ikke kan redigeres og ikke blir brukt. Vennligst opprett filteret på nytt med den nye uttrykkseditoren. Det utdaterte filteruttrykket var: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dette er API-nøkkelen din. Den kan brukes til enkelte skrivebeskyttede API-operasjoner og gir tilgang til Fever API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel"
|
msgstr "Dette er din API-nøkkel. Den kan brukes til noen skrivebeskyttede API-operasjoner og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Nettsted"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Velkommen! Det ser ut til å være første gang du kjører CommaFeed. Vennligst opprett en administratorkonto for å komme i gang."
|
msgstr "Velkommen! Det ser ut til å være første gang du kjører CommaFeed. Vennligst opprett en administratorkonto for å komme i gang."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gul"
|
msgstr "Gul"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed browser extensie versie {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed is compatibel met de Fever API. Gebruik de volgende URL in uw Fever-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>."
|
msgstr "CommaFeed is compatibel met de Fever API. Gebruik de volgende URL in uw Fever-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed is compatibel met de Google Reader API. Gebruik de volgende URL in uw Google Reader-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed volgende ongelezen item"
|
msgstr "CommaFeed volgende ongelezen item"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Ga naar de API-documentatie."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Goederen"
|
msgstr "Goederen"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Druif"
|
msgstr "Druif"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Deze feed heeft een legacy-filter dat niet kan worden bewerkt en niet wordt toegepast. Maak het filter opnieuw met de nieuwe expressie-editor. De legacy-filterexpressie was: <0>{0}</0>"
|
msgstr "Deze feed heeft een legacy-filter dat niet kan worden bewerkt en niet wordt toegepast. Maak het filter opnieuw met de nieuwe expressie-editor. De legacy-filterexpressie was: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dit is je API-sleutel. Het kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren"
|
msgstr "Dit is uw API-sleutel. Deze kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API en de Google Reader API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Website"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Welkom! Dit lijkt de eerste keer te zijn dat je CommaFeed draait. Maak een beheerdersaccount aan om aan de slag te gaan."
|
msgstr "Welkom! Dit lijkt de eerste keer te zijn dat je CommaFeed draait. Maak een beheerdersaccount aan om aan de slag te gaan."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Geel"
|
msgstr "Geel"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed nettlesarutviding versjon {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgjande URL i din Fever-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>."
|
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgjande URL i din Fever-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgjande URL i din Google Reader-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed neste ulesne element"
|
msgstr "CommaFeed neste ulesne element"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Godbitar"
|
msgstr "Godbitar"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Drue"
|
msgstr "Drue"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Denne suapen har eit gamalt filter som ikkje kan redigerast og som ikkje vert brukt. Ver venleg og lag filteret på nytt med den nye uttrykkseditoren. Det gamle filteruttrykket var: <0>{0}</0>"
|
msgstr "Denne suapen har eit gamalt filter som ikkje kan redigerast og som ikkje vert brukt. Ver venleg og lag filteret på nytt med den nye uttrykkseditoren. Det gamle filteruttrykket var: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Dette er API-nøkkelen din. Han kan brukast til enkelte skrivebeskytta API-handlingar og gjev tilgang til Fever API. Bruk skjemaet nedst på sida for å laga ein ny API-nøkkel"
|
msgstr "Dette er din API-nøkkel. Ho kan brukast til nokre skrivebeskytta API-operasjonar og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nedst på sida for å generere ein ny API-nøkkel"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Nettstad"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Velkomen! Det ser ut til å vera fyrste gongen du køyrer CommaFeed. Ver venleg og opprett ein administratorkonto for å koma i gang."
|
msgstr "Velkomen! Det ser ut til å vera fyrste gongen du køyrer CommaFeed. Ver venleg og opprett ein administratorkonto for å koma i gang."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gul"
|
msgstr "Gul"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Wersja rozszerzenia przeglądarki CommaFeed {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed jest kompatybilny z API Fever. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Fever. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>."
|
msgstr "CommaFeed jest kompatybilny z API Fever. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Fever. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed jest kompatybilny z API Google Reader. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Google Reader. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "Przecinek następny nieprzeczytany element"
|
msgstr "Przecinek następny nieprzeczytany element"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Przejdź do dokumentacji API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Gadżety"
|
msgstr "Gadżety"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Winogronowy"
|
msgstr "Winogronowy"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Ten kanał ma stary filtr, którego nie można edytować i który nie jest stosowany. Proszę utworzyć filtr ponownie, korzystając z nowego edytora wyrażeń. Stare wyrażenie filtra to: <0>{0}</0>"
|
msgstr "Ten kanał ma stary filtr, którego nie można edytować i który nie jest stosowany. Proszę utworzyć filtr ponownie, korzystając z nowego edytora wyrażeń. Stare wyrażenie filtra to: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever. Użyj formularza na dole strony, aby wygenerować nowy klucz API"
|
msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever i API Google Reader. Użyj formularza na dole strony, aby wygenerować nowy klucz API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Strona internetowa"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Witaj! Wygląda na to, że uruchamiasz CommaFeed po raz pierwszy. Aby rozpocząć, utwórz konto administratora."
|
msgstr "Witaj! Wygląda na to, że uruchamiasz CommaFeed po raz pierwszy. Aby rozpocząć, utwórz konto administratora."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Żółty"
|
msgstr "Żółty"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Extensão de navegador CommaFeed versão {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed é compatível com a API Fever. Use a URL a seguir no seu aplicativo móvel compatível com Fever. Entre com seu nome de usuário e sua <0>chave de API</0>."
|
msgstr "CommaFeed é compatível com a API Fever. Use a URL a seguir no seu aplicativo móvel compatível com Fever. Entre com seu nome de usuário e sua <0>chave de API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed é compatível com a API Google Reader. Use a URL a seguir no seu aplicativo móvel compatível com Google Reader. Entre com seu nome de usuário e sua <0>chave de API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed próximo item não lido"
|
msgstr "CommaFeed próximo item não lido"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Vá para a documentação da API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Brindes"
|
msgstr "Brindes"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Uva"
|
msgstr "Uva"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Este feed tem um filtro legado que não pode ser editado e não é aplicado. Recrie o filtro usando o novo editor de expressões. A expressão do filtro legado era: <0>{0}</0>"
|
msgstr "Este feed tem um filtro legado que não pode ser editado e não é aplicado. Recrie o filtro usando o novo editor de expressões. A expressão do filtro legado era: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações somente leitura da API e concede acesso à API do Fever. Use o formulário abaixo para gerar uma nova chave de API"
|
msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações de API somente leitura e concede acesso à API Fever e à API Google Reader. Use o formulário na parte inferior da página para gerar uma nova chave de API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Site"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Bem-vindo! Esta parece ser a primeira vez que você está executando o CommaFeed. Crie uma conta de administrador para começar."
|
msgstr "Bem-vindo! Esta parece ser a primeira vez que você está executando o CommaFeed. Crie uma conta de administrador para começar."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Amarelo"
|
msgstr "Amarelo"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Версия расширения браузера CommaFeed {browserExt
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed совместим с API Fever. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Fever. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>."
|
msgstr "CommaFeed совместим с API Fever. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Fever. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed совместим с API Google Reader. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Google Reader. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed следующий непрочитанный элемент"
|
msgstr "CommaFeed следующий непрочитанный элемент"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Перейдите к документации по API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Сладости"
|
msgstr "Сладости"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Виноградный"
|
msgstr "Виноградный"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Этот фид имеет устаревший фильтр, который нельзя редактировать и который не применяется. Пожалуйста, создайте фильтр заново, используя новый редактор выражений. Устаревшее выражение фильтра было: <0>{0}</0>"
|
msgstr "Этот фид имеет устаревший фильтр, который нельзя редактировать и который не применяется. Пожалуйста, создайте фильтр заново, используя новый редактор выражений. Устаревшее выражение фильтра было: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever. Чтобы сгенерировать новый ключ API, воспользуйтесь формой в нижней части страницы"
|
msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever и API Google Reader. Используйте форму внизу страницы, чтобы сгенерировать новый ключ API"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Веб-сайт"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Добро пожаловать! Похоже, вы запускаете CommaFeed в первый раз. Пожалуйста, создайте учетную запись администратора, чтобы начать работу."
|
msgstr "Добро пожаловать! Похоже, вы запускаете CommaFeed в первый раз. Пожалуйста, создайте учетную запись администратора, чтобы начать работу."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Желтый"
|
msgstr "Желтый"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Rozšírenie prehliadača CommaFeed verzie {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed je kompatibilný s Fever API. Vo svojom mobilnom klientovi kompatibilnom s Fever použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>."
|
msgstr "CommaFeed je kompatibilný s Fever API. Vo svojom mobilnom klientovi kompatibilnom s Fever použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed je kompatibilný s Google Reader API. Vo svojom mobilnom klientovi kompatibilnom s Google Reader použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed ďalšia neprečítaná položka"
|
msgstr "CommaFeed ďalšia neprečítaná položka"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Prejdite na dokumentáciu API."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Dobrôtky"
|
msgstr "Dobrôtky"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Hroznová"
|
msgstr "Hroznová"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Tento kanál má starší filter, ktorý sa nedá upravovať a nepoužíva sa. Znova vytvorte filter pomocou nového editora výrazov. Pôvodný výraz filtra bol: <0>{0}</0>"
|
msgstr "Tento kanál má starší filter, ktorý sa nedá upravovať a nepoužíva sa. Znova vytvorte filter pomocou nového editora výrazov. Pôvodný výraz filtra bol: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Toto je váš kľúč API. Môže sa použiť na niektoré operácie rozhrania API len na čítanie a poskytuje prístup k rozhraniu Fever API. Na vygenerovanie nového kľúča API použite formulár v dolnej časti stránky."
|
msgstr "Toto je váš kľúč API. Môže sa použiť pre niektoré operácie API iba na čítanie a udeľuje prístup k Fever API a Google Reader API. Na vygenerovanie nového kľúča API použite formulár v spodnej časti stránky"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Webová stránka"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Vitajte! Zdá sa, že CommaFeed spúšťate prvýkrát. Ak chcete začať, vytvorte si účet správcu."
|
msgstr "Vitajte! Zdá sa, že CommaFeed spúšťate prvýkrát. Ak chcete začať, vytvorte si účet správcu."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Žltá"
|
msgstr "Žltá"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "Webbläsartillägget CommaFeed version {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed är kompatibelt med Fever API. Använd följande URL i din Fever-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>."
|
msgstr "CommaFeed är kompatibelt med Fever API. Använd följande URL i din Fever-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed är kompatibelt med Google Reader API. Använd följande URL i din Google Reader-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed nästa olästa objekt"
|
msgstr "CommaFeed nästa olästa objekt"
|
||||||
@@ -454,6 +458,14 @@ msgstr "Gå till API-dokumentationen."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "Godsaker"
|
msgstr "Godsaker"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Lila"
|
msgstr "Lila"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Detta flöde har ett gammalt filter som inte kan redigeras och inte tillämpas. Skapa filtret på nytt med den nya uttryckseditorern. Det gamla filteruttrycket var: <0>{0}</0>"
|
msgstr "Detta flöde har ett gammalt filter som inte kan redigeras och inte tillämpas. Skapa filtret på nytt med den nya uttryckseditorern. Det gamla filteruttrycket var: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Detta är din API-nyckel. Den kan användas för vissa skrivskyddade API-operationer och ger åtkomst till Fever API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel"
|
msgstr "Det här är din API-nyckel. Den kan användas för vissa skrivskyddade API-åtgärder och ger åtkomst till Fever API och Google Reader API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Webbplats"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Välkommen! Det verkar vara första gången du kör CommaFeed. Skapa ett administratörskonto för att komma igång."
|
msgstr "Välkommen! Det verkar vara första gången du kör CommaFeed. Skapa ett administratörskonto för att komma igång."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gul"
|
msgstr "Gul"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed, Fever API ile uyumludur. Fever uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın."
|
msgstr "CommaFeed, Fever API ile uyumludur. Fever uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın."
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed, Google Reader API ile uyumludur. Google Reader uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın."
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed sonraki okunmamış öğe"
|
msgstr "CommaFeed sonraki okunmamış öğe"
|
||||||
@@ -454,6 +458,14 @@ msgstr "API belgelerine gidin."
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "İyilikler"
|
msgstr "İyilikler"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "Üzüm"
|
msgstr "Üzüm"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "Bu beslemenin düzenlenemeyen ve uygulanmayan eski bir filtresi var. Lütfen yeni ifade düzenleyiciyi kullanarak filtreyi yeniden oluşturun. Eski filtre ifadesi şuydu: <0>{0}</0>"
|
msgstr "Bu beslemenin düzenlenemeyen ve uygulanmayan eski bir filtresi var. Lütfen yeni ifade düzenleyiciyi kullanarak filtreyi yeniden oluşturun. Eski filtre ifadesi şuydu: <0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "Bu sizin API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API'sine erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın"
|
msgstr "Bu, API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API ile Google Reader API'ye erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "Web sitesi"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "Hoş geldiniz! Görünüşe göre CommaFeed'i ilk kez çalıştırıyorsunuz. Başlamak için lütfen bir yönetici hesabı oluşturun."
|
msgstr "Hoş geldiniz! Görünüşe göre CommaFeed'i ilk kez çalıştırıyorsunuz. Başlamak için lütfen bir yönetici hesabı oluşturun."
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Sarı"
|
msgstr "Sarı"
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ msgstr "CommaFeed的浏览器扩展版本:{browserExtensionVersion}。"
|
|||||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
msgstr "CommaFeed 和 Fever API 兼容,可以在您Fever兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。"
|
msgstr "CommaFeed 和 Fever API 兼容,可以在您Fever兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||||
|
msgstr "CommaFeed 和 Google Reader API 兼容,可以在您Google Reader兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。"
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed next unread item"
|
msgid "CommaFeed next unread item"
|
||||||
msgstr "CommaFeed 下一个未读项目"
|
msgstr "CommaFeed 下一个未读项目"
|
||||||
@@ -454,6 +458,14 @@ msgstr "API 文档"
|
|||||||
msgid "Goodies"
|
msgid "Goodies"
|
||||||
msgstr "好东西"
|
msgstr "好东西"
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
|
msgid "Google Reader API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Grape"
|
msgid "Grape"
|
||||||
msgstr "深紫"
|
msgstr "深紫"
|
||||||
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
|
|||||||
msgstr "此信息流具有无法编辑且未应用的旧版过滤器。请使用新的表达式编辑器重新创建过滤器。旧版过滤器表达式为:<0>{0}</0>"
|
msgstr "此信息流具有无法编辑且未应用的旧版过滤器。请使用新的表达式编辑器重新创建过滤器。旧版过滤器表达式为:<0>{0}</0>"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
|
||||||
msgstr "这是您的API 密钥,它可以被用于Fever API的只读操作及访问授权。使用页面底部的表单生成一个新的API密钥。"
|
msgstr "这是您的 API 密钥。它可用于某些只读 API 操作,并授予对 Fever API 和 Google Reader API 的访问权限。使用页面底部的表单生成新的 API 密钥"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
|
||||||
@@ -1138,6 +1150,10 @@ msgstr "网站"
|
|||||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||||
msgstr "欢迎!当前页仅当您第一次使用CommaFeed时出现,请创建一个管理员帐号以开始使用。"
|
msgstr "欢迎!当前页仅当您第一次使用CommaFeed时出现,请创建一个管理员帐号以开始使用。"
|
||||||
|
|
||||||
|
#: src/app/client.ts
|
||||||
|
msgid "Wrong username or password"
|
||||||
|
msgstr "用户名或密码错误"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "黄"
|
msgstr "黄"
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ dayjs.extend(relativeTime)
|
|||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
root &&
|
if (!root) throw new Error("root element not found")
|
||||||
ReactDOM.createRoot(root).render(
|
|
||||||
<Provider store={store}>
|
ReactDOM.createRoot(root).render(
|
||||||
<App />
|
<Provider store={store}>
|
||||||
</Provider>
|
<App />
|
||||||
)
|
</Provider>
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.2.1</version>
|
<version>7.3.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-server</artifactId>
|
<artifactId>commafeed-server</artifactId>
|
||||||
<name>CommaFeed Server</name>
|
<name>CommaFeed Server</name>
|
||||||
<packaging>quarkus</packaging>
|
<packaging>quarkus</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<quarkus.version>3.38.0</quarkus.version>
|
<quarkus.version>3.38.3</quarkus.version>
|
||||||
<querydsl.version>7.5</querydsl.version>
|
<querydsl.version>7.6</querydsl.version>
|
||||||
<rome.version>2.1.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
<!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
|
<!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
|
||||||
<google-java-format.version>1.36.1</google-java-format.version>
|
<google-java-format.version>1.36.1</google-java-format.version>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.protobuf</groupId>
|
<groupId>com.google.protobuf</groupId>
|
||||||
<artifactId>protobuf-java</artifactId>
|
<artifactId>protobuf-java</artifactId>
|
||||||
<version>4.35.1</version>
|
<version>4.36.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
<groupId>com.puppycrawl.tools</groupId>
|
||||||
<artifactId>checkstyle</artifactId>
|
<artifactId>checkstyle</artifactId>
|
||||||
<version>13.9.0</version>
|
<version>14.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<executions>
|
<executions>
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.diffplug.spotless</groupId>
|
<groupId>com.diffplug.spotless</groupId>
|
||||||
<artifactId>spotless-maven-plugin</artifactId>
|
<artifactId>spotless-maven-plugin</artifactId>
|
||||||
<version>3.9.0</version>
|
<version>3.10.0</version>
|
||||||
<?m2e ignore?>
|
<?m2e ignore?>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<version>7.2.1</version>
|
<version>7.3.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- compile-time processors -->
|
<!-- compile-time processors -->
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-collections4</artifactId>
|
<artifactId>commons-collections4</artifactId>
|
||||||
<version>4.5.0</version>
|
<version>4.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.cel</groupId>
|
<groupId>dev.cel</groupId>
|
||||||
<artifactId>cel</artifactId>
|
<artifactId>cel</artifactId>
|
||||||
<version>0.13.1</version>
|
<version>0.14.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.passay</groupId>
|
<groupId>org.passay</groupId>
|
||||||
@@ -449,7 +449,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
<artifactId>httpclient5</artifactId>
|
<artifactId>httpclient5</artifactId>
|
||||||
<version>5.6.3</version>
|
<version>5.6.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.hakky54</groupId>
|
<groupId>io.github.hakky54</groupId>
|
||||||
@@ -482,7 +482,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mock-server</groupId>
|
<groupId>org.mock-server</groupId>
|
||||||
<artifactId>mockserver-junit-jupiter</artifactId>
|
<artifactId>mockserver-junit-jupiter</artifactId>
|
||||||
<version>7.5.0</version>
|
<version>7.6.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ibm-semeru-runtimes:open-jdk-25.0.3.0-jre@sha256:43ec947fee9c4c2ed880620ae9e78911ac5eef66391e5a2c43f956dcc9000491
|
FROM ibm-semeru-runtimes:open-jdk-25.0.4.0-jre@sha256:524f0d04723aff42ed4142ab2621fa55f22cb691763cd75b59eee86749e2e872
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
RUN mkdir -p /commafeed/data
|
RUN mkdir -p /commafeed/data
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:13.6@sha256:fac46bff2e02f51425b6e33b0e1169f55dfb053d83511ca28aa50c09fd5ed7a4
|
FROM debian:13.6@sha256:34cd9e9fd437c0a095ec39cb2e73422c9f30821b0d0848ed74fd0d43bae4d958
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|||||||
@@ -93,3 +93,12 @@ Tags are of the form `<version>-<database>[-jvm]` where:
|
|||||||
- `master` (always points to the latest git commit)
|
- `master` (always points to the latest git commit)
|
||||||
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
||||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively.
|
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Getting "Access to local address blocked" when adding a feed
|
||||||
|
|
||||||
|
CommaFeed blocks access to local resources by default to prevent [SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
|
||||||
|
If you want to subscribe to feeds that are only available on your local network, you can disable this security measure by setting the `COMMAFEED_HTTP_CLIENT_BLOCK_LOCAL_ADDRESSES` variable to `false`.
|
||||||
|
Do this only if you trust all users of your CommaFeed instance not to access private resources.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import jakarta.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.SystemProperties;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -26,6 +28,12 @@ public class CommaFeedApplication {
|
|||||||
public void start(@Observes StartupEvent ev) {
|
public void start(@Observes StartupEvent ev) {
|
||||||
log.info("starting up...");
|
log.info("starting up...");
|
||||||
|
|
||||||
|
// disable entity expansion limits added in JDK24+ (#1961)
|
||||||
|
// we already strip doctype declarations in XMLCleaner to prevent xxe attacks
|
||||||
|
// we also already limit the size of feeds we download in HttpGetter
|
||||||
|
System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
|
||||||
|
System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
|
||||||
|
|
||||||
PasswordConstraintValidator.setMinimumPasswordLength(
|
PasswordConstraintValidator.setMinimumPasswordLength(
|
||||||
config.users().minimumPasswordLength());
|
config.users().minimumPasswordLength());
|
||||||
|
|
||||||
@@ -33,6 +41,11 @@ public class CommaFeedApplication {
|
|||||||
ImageProxyUrl.generateKey();
|
ImageProxyUrl.generateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.passwordRecoveryEnabled() && config.passwordRecoveryPublicBaseUrl().isEmpty()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"password recovery is enabled but no public base url is set");
|
||||||
|
}
|
||||||
|
|
||||||
feedRefreshEngine.start();
|
feedRefreshEngine.start();
|
||||||
taskScheduler.start();
|
taskScheduler.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ public interface CommaFeedConfiguration {
|
|||||||
@WithDefault("false")
|
@WithDefault("false")
|
||||||
boolean passwordRecoveryEnabled();
|
boolean passwordRecoveryEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base URL of the application to use in the password recovery link in the email. We can't
|
||||||
|
* use the URL sent by the browser of the user because of malicious Host header injection
|
||||||
|
* attacks.
|
||||||
|
*/
|
||||||
|
Optional<String> passwordRecoveryPublicBaseUrl();
|
||||||
|
|
||||||
/** Message displayed in a notification at the bottom of the page. */
|
/** Message displayed in a notification at the bottom of the page. */
|
||||||
Optional<String> announcement();
|
Optional<String> announcement();
|
||||||
|
|
||||||
@@ -130,7 +137,7 @@ public interface CommaFeedConfiguration {
|
|||||||
* <p>You may want to enable this if you host a public instance of CommaFeed with
|
* <p>You may want to enable this if you host a public instance of CommaFeed with
|
||||||
* registrations open.
|
* registrations open.
|
||||||
*/
|
*/
|
||||||
@WithDefault("false")
|
@WithDefault("true")
|
||||||
boolean blockLocalAddresses();
|
boolean blockLocalAddresses();
|
||||||
|
|
||||||
/** HTTP client cache configuration */
|
/** HTTP client cache configuration */
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.commafeed;
|
package com.commafeed;
|
||||||
|
|
||||||
|
import com.commafeed.frontend.exception.CommaFeedApplicationException;
|
||||||
|
import com.commafeed.frontend.exception.CommaFeedExceptionType;
|
||||||
import com.commafeed.security.CookieService;
|
import com.commafeed.security.CookieService;
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
@@ -26,6 +28,18 @@ public class ExceptionMappers {
|
|||||||
private final CookieService cookieService;
|
private final CookieService cookieService;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
|
@ServerExceptionMapper(CommaFeedApplicationException.class)
|
||||||
|
public RestResponse<CommaFeedApplicationError> applicationError(
|
||||||
|
CommaFeedApplicationException e) {
|
||||||
|
ResponseBuilder<CommaFeedApplicationError> response =
|
||||||
|
ResponseBuilder.create(
|
||||||
|
e.type().status(), new CommaFeedApplicationError(e.type(), e.getMessage()));
|
||||||
|
if (e.type().status() == Status.UNAUTHORIZED) {
|
||||||
|
response.cookie(cookieService.buildLogoutCookie());
|
||||||
|
}
|
||||||
|
return response.build();
|
||||||
|
}
|
||||||
|
|
||||||
@ServerExceptionMapper(UnauthorizedException.class)
|
@ServerExceptionMapper(UnauthorizedException.class)
|
||||||
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
|
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
|
||||||
return RestResponse.status(
|
return RestResponse.status(
|
||||||
@@ -55,4 +69,7 @@ public class ExceptionMappers {
|
|||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
public record ValidationFailed(String message) {}
|
public record ValidationFailed(String message) {}
|
||||||
|
|
||||||
|
@RegisterForReflection
|
||||||
|
public record CommaFeedApplicationError(CommaFeedExceptionType type, String message) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.google.common.net.HttpHeaders;
|
|||||||
import inet.ipaddr.IPAddress;
|
import inet.ipaddr.IPAddress;
|
||||||
import inet.ipaddr.IPAddressNetwork;
|
import inet.ipaddr.IPAddressNetwork;
|
||||||
import inet.ipaddr.IPAddressString;
|
import inet.ipaddr.IPAddressString;
|
||||||
|
import inet.ipaddr.ipv4.IPv4Address;
|
||||||
|
import inet.ipaddr.ipv6.IPv6Address;
|
||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
@@ -137,8 +139,38 @@ public class HttpClientFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isLocalAddress(InetAddress address) {
|
private static boolean isLocalAddress(InetAddress address) {
|
||||||
IPAddress ip = new IPAddressNetwork.IPAddressGenerator().from(address);
|
return isLocalAddress(new IPAddressNetwork.IPAddressGenerator().from(address));
|
||||||
return ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip);
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalAddress(IPAddress ip) {
|
||||||
|
if (ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ip.isIPv6()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 transition mechanisms embed an IPv4 address that must be validated too, otherwise
|
||||||
|
// they could be used to smuggle a blocked IPv4 target past the IPv6-only checks above
|
||||||
|
IPv6Address ipv6 = ip.toIPv6();
|
||||||
|
if (ipv6.isIPv4Mapped() || ipv6.isIPv4Compatible() || ipv6.isWellKnownIPv4Translatable()) {
|
||||||
|
// IPv4-mapped (::ffff:x.x.x.x), IPv4-compatible (::x.x.x.x) and NAT64
|
||||||
|
// (64:ff9b::/96, RFC 6052) addresses all embed the IPv4 address in the lowest 32 bits
|
||||||
|
return isLocalAddress(ipv6.getEmbeddedIPv4Address());
|
||||||
|
}
|
||||||
|
if (ipv6.is6To4()) {
|
||||||
|
// 6to4 (2002::/16, RFC 3056) embeds the IPv4 address in bits 16-47
|
||||||
|
return isLocalAddress(ipv6.get6To4IPv4Address());
|
||||||
|
}
|
||||||
|
if (ipv6.isTeredo()) {
|
||||||
|
// Teredo (2001::/32, RFC 4380) embeds the IPv4 address in the lowest 32 bits,
|
||||||
|
// obfuscated with a bitwise complement
|
||||||
|
IPv4Address obfuscated = ipv6.getEmbeddedIPv4Address();
|
||||||
|
return isLocalAddress(new IPv4Address(~obfuscated.intValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {
|
private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import jakarta.inject.Singleton;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.SystemProperties;
|
|
||||||
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
||||||
import org.jdom2.Element;
|
import org.jdom2.Element;
|
||||||
import org.jdom2.Namespace;
|
import org.jdom2.Namespace;
|
||||||
@@ -58,12 +57,6 @@ public class FeedParser {
|
|||||||
public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
|
public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
|
||||||
this.encodingDetector = encodingDetector;
|
this.encodingDetector = encodingDetector;
|
||||||
this.xmlCleaner = xmlCleaner;
|
this.xmlCleaner = xmlCleaner;
|
||||||
|
|
||||||
// disable entity expansion limits added in JDK24+ (#1961)
|
|
||||||
// we already strip doctype declarations in XMLCleaner to prevent xxe attacks
|
|
||||||
// we also already limit the size of feeds we download in HttpGetter
|
|
||||||
System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
|
|
||||||
System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException {
|
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException {
|
||||||
|
|||||||
@@ -98,15 +98,14 @@ public class FeedEntryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) {
|
public void starEntry(User user, Long entryId, boolean starred) {
|
||||||
|
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
|
if (entry == null) {
|
||||||
if (sub == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
||||||
if (entry == null) {
|
if (sub == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.commafeed.backend.service;
|
|||||||
|
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryTag;
|
import com.commafeed.backend.model.FeedEntryTag;
|
||||||
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
@@ -20,6 +22,7 @@ public class FeedEntryTagService {
|
|||||||
|
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
private final FeedEntryDAO feedEntryDAO;
|
||||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||||
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
|
|
||||||
public void updateTags(User user, Long entryId, List<String> tagNames) {
|
public void updateTags(User user, Long entryId, List<String> tagNames) {
|
||||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||||
@@ -27,6 +30,11 @@ public class FeedEntryTagService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
||||||
|
if (sub == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
|
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
|
||||||
Set<String> existingTagNames =
|
Set<String> existingTagNames =
|
||||||
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());
|
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.commafeed.frontend.exception;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class CommaFeedApplicationException extends RuntimeException {
|
||||||
|
|
||||||
|
@Serial private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final CommaFeedExceptionType type;
|
||||||
|
|
||||||
|
public CommaFeedApplicationException(CommaFeedExceptionType type) {
|
||||||
|
super(Objects.requireNonNull(type).message());
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommaFeedExceptionType type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.commafeed.frontend.exception;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.reactive.RestResponse.Status;
|
||||||
|
|
||||||
|
public enum CommaFeedExceptionType {
|
||||||
|
WRONG_USERNAME_OR_PASSWORD(Status.UNAUTHORIZED, "wrong username or password");
|
||||||
|
|
||||||
|
private final Status status;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
CommaFeedExceptionType(Status status, String message) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status status() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String message() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,6 @@ public class StarRequest implements Serializable {
|
|||||||
@Size(max = 128)
|
@Size(max = 128)
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@Schema(description = "feed id", required = true)
|
|
||||||
private Long feedId;
|
|
||||||
|
|
||||||
@Schema(description = "starred or not", required = true)
|
@Schema(description = "starred or not", required = true)
|
||||||
private boolean starred;
|
private boolean starred;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,11 +89,9 @@ public class EntryREST {
|
|||||||
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||||
Preconditions.checkNotNull(req);
|
Preconditions.checkNotNull(req);
|
||||||
Preconditions.checkNotNull(req.getId());
|
Preconditions.checkNotNull(req.getId());
|
||||||
Preconditions.checkNotNull(req.getFeedId());
|
|
||||||
|
|
||||||
User user = authenticationContext.getCurrentUser();
|
User user = authenticationContext.getCurrentUser();
|
||||||
feedEntryService.starEntry(
|
feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.isStarred());
|
||||||
user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
|
|
||||||
|
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,7 +413,8 @@ public class UserREST {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String buildEmailContent(User user) throws URISyntaxException {
|
private String buildEmailContent(User user) throws URISyntaxException {
|
||||||
String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString());
|
String publicUrl =
|
||||||
|
Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow());
|
||||||
return String.format(
|
return String.format(
|
||||||
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||||
user.getName(), callbackUrl(user, publicUrl));
|
user.getName(), callbackUrl(user, publicUrl));
|
||||||
|
|||||||
@@ -388,9 +388,7 @@ public class FeverREST {
|
|||||||
if ("read".equals(action) || "unread".equals(action)) {
|
if ("read".equals(action) || "unread".equals(action)) {
|
||||||
feedEntryService.markEntry(user, id, "read".equals(action));
|
feedEntryService.markEntry(user, id, "read".equals(action));
|
||||||
} else if ("saved".equals(action) || "unsaved".equals(action)) {
|
} else if ("saved".equals(action) || "unsaved".equals(action)) {
|
||||||
FeedEntry entry = feedEntryDAO.findById(id);
|
feedEntryService.starEntry(user, id, "saved".equals(action));
|
||||||
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
|
||||||
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
|
|
||||||
}
|
}
|
||||||
} else if ("feed".equals(source)) {
|
} else if ("feed".equals(source)) {
|
||||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package com.commafeed.frontend.resource.googlereader;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonInclude(Include.NON_NULL)
|
||||||
|
public class GoogleReaderModel {
|
||||||
|
|
||||||
|
private GoogleReaderModel() {}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UserInfo {
|
||||||
|
|
||||||
|
@JsonProperty("userId")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@JsonProperty("userName")
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@JsonProperty("userProfileId")
|
||||||
|
private String userProfileId;
|
||||||
|
|
||||||
|
@JsonProperty("userEmail")
|
||||||
|
private String userEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class SubscriptionList {
|
||||||
|
|
||||||
|
@JsonProperty("subscriptions")
|
||||||
|
private List<Subscription> subscriptions = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Subscription {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty("categories")
|
||||||
|
private List<Category> categories = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("url")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@JsonProperty("htmlUrl")
|
||||||
|
private String htmlUrl;
|
||||||
|
|
||||||
|
@JsonProperty("iconUrl")
|
||||||
|
private String iconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Category {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("label")
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TagList {
|
||||||
|
|
||||||
|
@JsonProperty("tags")
|
||||||
|
private List<Tag> tags = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Tag {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class StreamContents {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty("updated")
|
||||||
|
private long updated;
|
||||||
|
|
||||||
|
@JsonProperty("items")
|
||||||
|
private List<StreamItem> items = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("continuation")
|
||||||
|
private String continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class StreamItem {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("crawlTimeMsec")
|
||||||
|
private String crawlTimeMsec;
|
||||||
|
|
||||||
|
@JsonProperty("timestampUsec")
|
||||||
|
private String timestampUsec;
|
||||||
|
|
||||||
|
@JsonProperty("published")
|
||||||
|
private long published;
|
||||||
|
|
||||||
|
@JsonProperty("updated")
|
||||||
|
private long updated;
|
||||||
|
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty("author")
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
@JsonProperty("categories")
|
||||||
|
private List<String> categories = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("canonical")
|
||||||
|
private List<HrefEntry> canonical = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("alternate")
|
||||||
|
private List<HrefEntry> alternate = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("summary")
|
||||||
|
private Content summary;
|
||||||
|
|
||||||
|
@JsonProperty("origin")
|
||||||
|
private Origin origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class HrefEntry {
|
||||||
|
|
||||||
|
@JsonProperty("href")
|
||||||
|
private String href;
|
||||||
|
|
||||||
|
@JsonProperty("type")
|
||||||
|
private String type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Content {
|
||||||
|
|
||||||
|
@JsonProperty("content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@JsonProperty("direction")
|
||||||
|
private String direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Origin {
|
||||||
|
|
||||||
|
@JsonProperty("streamId")
|
||||||
|
private String streamId;
|
||||||
|
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty("htmlUrl")
|
||||||
|
private String htmlUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ItemRefs {
|
||||||
|
|
||||||
|
@JsonProperty("itemRefs")
|
||||||
|
private List<ItemRef> itemRefs = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("continuation")
|
||||||
|
private String continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ItemRef {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UnreadCounts {
|
||||||
|
|
||||||
|
@JsonProperty("max")
|
||||||
|
private long max;
|
||||||
|
|
||||||
|
@JsonProperty("unreadcounts")
|
||||||
|
private List<UnreadCountEntry> unreadCounts = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UnreadCountEntry {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("count")
|
||||||
|
private long count;
|
||||||
|
|
||||||
|
@JsonProperty("newestItemTimestampUsec")
|
||||||
|
private String newestItemTimestampUsec;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,829 @@
|
|||||||
|
package com.commafeed.frontend.resource.googlereader;
|
||||||
|
|
||||||
|
import com.commafeed.backend.Digests;
|
||||||
|
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
|
import com.commafeed.backend.model.FeedEntryContent;
|
||||||
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
|
import com.commafeed.backend.model.User;
|
||||||
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
|
import com.commafeed.backend.service.FeedEntryService;
|
||||||
|
import com.commafeed.backend.service.FeedSubscriptionService;
|
||||||
|
import com.commafeed.backend.service.UserService;
|
||||||
|
import com.commafeed.frontend.model.UnreadCount;
|
||||||
|
import com.commafeed.security.AuthenticationContext;
|
||||||
|
import com.commafeed.security.Roles;
|
||||||
|
import com.commafeed.security.mechanism.GoogleReaderAuthenticationMechanism;
|
||||||
|
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.FormParam;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Reader compatible API
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>url: /rest/googlereader
|
||||||
|
* <li>login: username
|
||||||
|
* <li>password: api key
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Path(GoogleReaderAuthenticationMechanism.PATH_PREFIX)
|
||||||
|
@RolesAllowed(Roles.USER)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Singleton
|
||||||
|
public class GoogleReaderREST {
|
||||||
|
|
||||||
|
private static final int DEFAULT_ITEM_COUNT = 20;
|
||||||
|
private static final int MAX_ITEM_COUNT = 1000;
|
||||||
|
// cap at which clients should display "count+" instead of the exact unread count
|
||||||
|
private static final int MAX_UNREAD_COUNT = 10000;
|
||||||
|
|
||||||
|
private final AuthenticationContext authenticationContext;
|
||||||
|
private final UserService userService;
|
||||||
|
private final FeedSubscriptionService feedSubscriptionService;
|
||||||
|
private final FeedEntryService feedEntryService;
|
||||||
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
|
private final FeedCategoryDAO feedCategoryDAO;
|
||||||
|
private final FeedEntryDAO feedEntryDAO;
|
||||||
|
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@PermitAll
|
||||||
|
@Path("/")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response welcome() {
|
||||||
|
return Response.ok("This is the Google Reader API endpoint.").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@PermitAll
|
||||||
|
@Path("/accounts/ClientLogin")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response clientLogin(@Context UriInfo uri, MultivaluedMap<String, String> form) {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||||
|
form.forEach((k, v) -> params.put(k, v.getFirst()));
|
||||||
|
|
||||||
|
String password = params.get("Passwd");
|
||||||
|
User user = StringUtils.isBlank(password) ? null : userService.login(password).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return Response.status(Status.FORBIDDEN).entity("Error=BadAuthentication\n").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = user.getName() + "/" + user.getApiKey();
|
||||||
|
String body = "SID=" + token + "\nLSID=" + token + "\nAuth=" + token + "\n";
|
||||||
|
return Response.ok(body).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/token")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response token() {
|
||||||
|
return Response.ok(computeEditToken(currentUser())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/user-info")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.UserInfo userInfo() {
|
||||||
|
User user = currentUser();
|
||||||
|
GoogleReaderModel.UserInfo info = new GoogleReaderModel.UserInfo();
|
||||||
|
info.setUserId(String.valueOf(user.getId()));
|
||||||
|
info.setUserName(user.getName());
|
||||||
|
info.setUserProfileId(String.valueOf(user.getId()));
|
||||||
|
info.setUserEmail(user.getEmail());
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/subscription/list")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.SubscriptionList subscriptionList() {
|
||||||
|
User user = currentUser();
|
||||||
|
GoogleReaderModel.SubscriptionList resp = new GoogleReaderModel.SubscriptionList();
|
||||||
|
resp.setSubscriptions(
|
||||||
|
feedSubscriptionDAO.findAll(user).stream().map(this::mapSubscription).toList());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/reader/api/0/subscription/edit")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response subscriptionEdit(
|
||||||
|
@FormParam("ac") String action,
|
||||||
|
@FormParam("s") String streamId,
|
||||||
|
@FormParam("t") String title,
|
||||||
|
@FormParam("a") String addLabel,
|
||||||
|
@FormParam("r") String removeLabel,
|
||||||
|
@FormParam("T") String token) {
|
||||||
|
User user = currentUser();
|
||||||
|
if (!validToken(user, token)) {
|
||||||
|
return Response.status(Status.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(action) || StringUtils.isBlank(streamId)) {
|
||||||
|
return Response.status(Status.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("subscribe".equals(action)) {
|
||||||
|
String url =
|
||||||
|
streamId.startsWith(GoogleReaderStreamId.FEED_PREFIX)
|
||||||
|
? streamId.substring(GoogleReaderStreamId.FEED_PREFIX.length())
|
||||||
|
: streamId;
|
||||||
|
FeedCategory category = resolveLabelCategory(user, addLabel, true);
|
||||||
|
String subTitle = StringUtils.defaultIfBlank(title, url);
|
||||||
|
feedSubscriptionService.subscribe(user, url, subTitle, category, 0);
|
||||||
|
} else {
|
||||||
|
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
|
||||||
|
Long subId = parsed.feedSubscriptionId();
|
||||||
|
if (subId == null) {
|
||||||
|
return Response.status(Status.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("unsubscribe".equals(action)) {
|
||||||
|
feedSubscriptionService.unsubscribe(user, subId);
|
||||||
|
} else if ("edit".equals(action)) {
|
||||||
|
FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
|
||||||
|
if (sub != null) {
|
||||||
|
if (StringUtils.isNotBlank(title)) {
|
||||||
|
sub.setTitle(title);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(addLabel)) {
|
||||||
|
sub.setCategory(resolveLabelCategory(user, addLabel, true));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(removeLabel)) {
|
||||||
|
sub.setCategory(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok("OK").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/tag/list")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.TagList tagList() {
|
||||||
|
User user = currentUser();
|
||||||
|
List<GoogleReaderModel.Tag> tags = new ArrayList<>();
|
||||||
|
feedCategoryDAO
|
||||||
|
.findAll(user)
|
||||||
|
.forEach(
|
||||||
|
c ->
|
||||||
|
tags.add(
|
||||||
|
new GoogleReaderModel.Tag(
|
||||||
|
GoogleReaderStreamId.label(c.getName()))));
|
||||||
|
tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED));
|
||||||
|
|
||||||
|
GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList();
|
||||||
|
resp.setTags(tags);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/unread-count")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.UnreadCounts unreadCounts() {
|
||||||
|
User user = currentUser();
|
||||||
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
|
Map<Long, UnreadCount> unreadCounts = feedSubscriptionService.getUnreadCount(user);
|
||||||
|
|
||||||
|
List<GoogleReaderModel.UnreadCountEntry> entries = new ArrayList<>();
|
||||||
|
Map<Long, Long> unreadByCategory = new HashMap<>();
|
||||||
|
long totalUnread = 0;
|
||||||
|
|
||||||
|
for (FeedSubscription sub : subs) {
|
||||||
|
UnreadCount uc = unreadCounts.get(sub.getId());
|
||||||
|
|
||||||
|
GoogleReaderModel.UnreadCountEntry entry = new GoogleReaderModel.UnreadCountEntry();
|
||||||
|
entry.setId(GoogleReaderStreamId.feed(sub.getId()));
|
||||||
|
entry.setCount(uc.getUnreadCount());
|
||||||
|
entry.setNewestItemTimestampUsec(toUsec(uc.getNewestItemTime()));
|
||||||
|
entries.add(entry);
|
||||||
|
|
||||||
|
totalUnread += uc.getUnreadCount();
|
||||||
|
|
||||||
|
long categoryId = sub.getCategory() == null ? 0 : sub.getCategory().getId();
|
||||||
|
unreadByCategory.merge(categoryId, uc.getUnreadCount(), Long::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, FeedCategory> categoriesById =
|
||||||
|
feedCategoryDAO.findAll(user).stream()
|
||||||
|
.collect(Collectors.toMap(FeedCategory::getId, c -> c));
|
||||||
|
unreadByCategory.forEach(
|
||||||
|
(categoryId, count) -> {
|
||||||
|
if (categoryId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FeedCategory category = categoriesById.get(categoryId);
|
||||||
|
if (category == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GoogleReaderModel.UnreadCountEntry entry =
|
||||||
|
new GoogleReaderModel.UnreadCountEntry();
|
||||||
|
entry.setId(GoogleReaderStreamId.label(category.getName()));
|
||||||
|
entry.setCount(count);
|
||||||
|
entries.add(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
GoogleReaderModel.UnreadCountEntry readingList = new GoogleReaderModel.UnreadCountEntry();
|
||||||
|
readingList.setId(GoogleReaderStreamId.READING_LIST);
|
||||||
|
readingList.setCount(totalUnread);
|
||||||
|
entries.add(readingList);
|
||||||
|
|
||||||
|
GoogleReaderModel.UnreadCounts resp = new GoogleReaderModel.UnreadCounts();
|
||||||
|
resp.setMax(MAX_UNREAD_COUNT);
|
||||||
|
resp.setUnreadCounts(entries);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/stream/contents/{streamId : .+}")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.StreamContents streamContentsPath(
|
||||||
|
@PathParam("streamId") String streamId,
|
||||||
|
@QueryParam("n") Integer n,
|
||||||
|
@QueryParam("c") String continuation,
|
||||||
|
@QueryParam("r") String order,
|
||||||
|
@QueryParam("xt") List<String> excludeTargets) {
|
||||||
|
return buildStreamContents(streamId, n, continuation, order, excludeTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/stream/contents")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.StreamContents streamContentsQuery(
|
||||||
|
@QueryParam("s") String streamId,
|
||||||
|
@QueryParam("n") Integer n,
|
||||||
|
@QueryParam("c") String continuation,
|
||||||
|
@QueryParam("r") String order,
|
||||||
|
@QueryParam("xt") List<String> excludeTargets) {
|
||||||
|
return buildStreamContents(streamId, n, continuation, order, excludeTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/stream/items/ids")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.ItemRefs streamItemIds(
|
||||||
|
@QueryParam("s") String streamId,
|
||||||
|
@QueryParam("n") Integer n,
|
||||||
|
@QueryParam("c") String continuation,
|
||||||
|
@QueryParam("r") String order,
|
||||||
|
@QueryParam("xt") List<String> excludeTargets) {
|
||||||
|
User user = currentUser();
|
||||||
|
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
|
||||||
|
int limit = clampLimit(n);
|
||||||
|
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
|
||||||
|
boolean unreadOnly = isUnreadOnly(excludeTargets);
|
||||||
|
|
||||||
|
PagedStatuses paged =
|
||||||
|
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||||
|
|
||||||
|
GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs();
|
||||||
|
resp.setItemRefs(
|
||||||
|
paged.statuses().stream()
|
||||||
|
.map(
|
||||||
|
s ->
|
||||||
|
new GoogleReaderModel.ItemRef(
|
||||||
|
String.valueOf(s.getEntry().getId())))
|
||||||
|
.toList());
|
||||||
|
resp.setContinuation(paged.continuation());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/reader/api/0/stream/items/contents")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.StreamContents streamItemsContentsPost(
|
||||||
|
@FormParam("i") List<String> ids) {
|
||||||
|
return buildStreamItemsContents(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/reader/api/0/stream/items/contents")
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public GoogleReaderModel.StreamContents streamItemsContentsGet(
|
||||||
|
@QueryParam("i") List<String> ids) {
|
||||||
|
return buildStreamItemsContents(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/reader/api/0/edit-tag")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response editTag(
|
||||||
|
@FormParam("i") List<String> ids,
|
||||||
|
@FormParam("a") List<String> add,
|
||||||
|
@FormParam("r") List<String> remove,
|
||||||
|
@FormParam("T") String token) {
|
||||||
|
User user = currentUser();
|
||||||
|
if (!validToken(user, token)) {
|
||||||
|
return Response.status(Status.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
if (ids == null) {
|
||||||
|
return Response.ok("OK").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean markRead = containsTag(add, GoogleReaderStreamId.READ);
|
||||||
|
boolean markUnread = containsTag(remove, GoogleReaderStreamId.READ);
|
||||||
|
boolean star = containsTag(add, GoogleReaderStreamId.STARRED);
|
||||||
|
boolean unstar = containsTag(remove, GoogleReaderStreamId.STARRED);
|
||||||
|
|
||||||
|
for (String rawId : ids) {
|
||||||
|
Long entryId = parseItemId(rawId);
|
||||||
|
if (entryId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markRead) {
|
||||||
|
feedEntryService.markEntry(user, entryId, true);
|
||||||
|
}
|
||||||
|
if (markUnread) {
|
||||||
|
feedEntryService.markEntry(user, entryId, false);
|
||||||
|
}
|
||||||
|
if (star) {
|
||||||
|
feedEntryService.starEntry(user, entryId, true);
|
||||||
|
}
|
||||||
|
if (unstar) {
|
||||||
|
feedEntryService.starEntry(user, entryId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok("OK").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/reader/api/0/mark-all-as-read")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Transactional
|
||||||
|
@Operation(hidden = true)
|
||||||
|
public Response markAllAsRead(
|
||||||
|
@FormParam("s") String streamId,
|
||||||
|
@FormParam("ts") Long ts,
|
||||||
|
@FormParam("T") String token) {
|
||||||
|
User user = currentUser();
|
||||||
|
if (!validToken(user, token)) {
|
||||||
|
return Response.status(Status.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
|
||||||
|
Instant olderThan = (ts != null && ts > 0) ? Instant.ofEpochSecond(ts / 1_000_000) : null;
|
||||||
|
|
||||||
|
switch (parsed.type()) {
|
||||||
|
case STARRED -> feedEntryService.markStarredEntries(user, olderThan, null);
|
||||||
|
case FEED -> {
|
||||||
|
Long subId = parsed.feedSubscriptionId();
|
||||||
|
FeedSubscription sub =
|
||||||
|
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||||
|
if (sub != null) {
|
||||||
|
feedEntryService.markSubscriptionEntries(
|
||||||
|
user, List.of(sub), olderThan, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case LABEL -> {
|
||||||
|
FeedCategory category = findCategoryByName(user, parsed.value());
|
||||||
|
if (category != null) {
|
||||||
|
List<FeedCategory> categories =
|
||||||
|
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||||
|
List<FeedSubscription> subs =
|
||||||
|
feedSubscriptionDAO.findByCategories(user, categories);
|
||||||
|
feedEntryService.markSubscriptionEntries(user, subs, olderThan, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default ->
|
||||||
|
feedEntryService.markSubscriptionEntries(
|
||||||
|
user, feedSubscriptionDAO.findAll(user), olderThan, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok("OK").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoogleReaderModel.StreamContents buildStreamContents(
|
||||||
|
String rawStreamId,
|
||||||
|
Integer n,
|
||||||
|
String continuation,
|
||||||
|
String order,
|
||||||
|
List<String> excludeTargets) {
|
||||||
|
User user = currentUser();
|
||||||
|
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(rawStreamId);
|
||||||
|
int limit = clampLimit(n);
|
||||||
|
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
|
||||||
|
boolean unreadOnly = isUnreadOnly(excludeTargets);
|
||||||
|
|
||||||
|
PagedStatuses paged =
|
||||||
|
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||||
|
|
||||||
|
GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents();
|
||||||
|
resp.setId(streamIdString(parsed));
|
||||||
|
resp.setTitle(streamTitle(user, parsed));
|
||||||
|
resp.setUpdated(Instant.now().getEpochSecond());
|
||||||
|
resp.setItems(paged.statuses().stream().map(this::mapItem).toList());
|
||||||
|
resp.setContinuation(paged.continuation());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoogleReaderModel.StreamContents buildStreamItemsContents(List<String> ids) {
|
||||||
|
User user = currentUser();
|
||||||
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
|
Map<Long, FeedSubscription> subsByFeedId =
|
||||||
|
subs.stream()
|
||||||
|
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s, (a, b) -> a));
|
||||||
|
|
||||||
|
List<GoogleReaderModel.StreamItem> items = new ArrayList<>();
|
||||||
|
if (ids != null) {
|
||||||
|
for (String rawId : ids) {
|
||||||
|
Long entryId = parseItemId(rawId);
|
||||||
|
if (entryId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FeedSubscription sub = subsByFeedId.get(entry.getFeed().getId());
|
||||||
|
if (sub == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||||
|
items.add(mapItem(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents();
|
||||||
|
resp.setId(GoogleReaderStreamId.READING_LIST);
|
||||||
|
resp.setUpdated(Instant.now().getEpochSecond());
|
||||||
|
resp.setItems(items);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PagedStatuses fetchStatuses(
|
||||||
|
User user,
|
||||||
|
GoogleReaderStreamId streamId,
|
||||||
|
int limit,
|
||||||
|
String continuation,
|
||||||
|
ReadingOrder order,
|
||||||
|
boolean unreadOnly) {
|
||||||
|
Long minEntryId = null;
|
||||||
|
Long maxEntryId = null;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
if (streamId.type() == GoogleReaderStreamId.Type.STARRED) {
|
||||||
|
offset = parseOffset(continuation);
|
||||||
|
} else if (StringUtils.isNotBlank(continuation)) {
|
||||||
|
try {
|
||||||
|
long cursor = Long.parseLong(continuation);
|
||||||
|
if (order == ReadingOrder.ASC) {
|
||||||
|
minEntryId = cursor;
|
||||||
|
} else {
|
||||||
|
maxEntryId = cursor;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// invalid continuation token, ignore and start from the beginning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FeedEntryStatus> statuses;
|
||||||
|
int fetchedCount;
|
||||||
|
switch (streamId.type()) {
|
||||||
|
case STARRED -> {
|
||||||
|
List<FeedEntryStatus> starred =
|
||||||
|
feedEntryStatusDAO.findStarred(
|
||||||
|
user, null, null, offset, limit, order, true);
|
||||||
|
fetchedCount = starred.size();
|
||||||
|
statuses =
|
||||||
|
unreadOnly ? starred.stream().filter(s -> !s.isRead()).toList() : starred;
|
||||||
|
}
|
||||||
|
case FEED -> {
|
||||||
|
Long subId = streamId.feedSubscriptionId();
|
||||||
|
FeedSubscription sub =
|
||||||
|
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||||
|
List<FeedSubscription> subs = sub == null ? List.of() : List.of(sub);
|
||||||
|
statuses =
|
||||||
|
feedEntryStatusDAO.findBySubscriptions(
|
||||||
|
user,
|
||||||
|
subs,
|
||||||
|
unreadOnly,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
limit,
|
||||||
|
order,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
minEntryId,
|
||||||
|
maxEntryId);
|
||||||
|
fetchedCount = statuses.size();
|
||||||
|
}
|
||||||
|
case LABEL -> {
|
||||||
|
FeedCategory category = findCategoryByName(user, streamId.value());
|
||||||
|
List<FeedSubscription> subs =
|
||||||
|
category == null
|
||||||
|
? List.of()
|
||||||
|
: feedSubscriptionDAO.findByCategories(
|
||||||
|
user,
|
||||||
|
feedCategoryDAO.findAllChildrenCategories(user, category));
|
||||||
|
statuses =
|
||||||
|
feedEntryStatusDAO.findBySubscriptions(
|
||||||
|
user,
|
||||||
|
subs,
|
||||||
|
unreadOnly,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
limit,
|
||||||
|
order,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
minEntryId,
|
||||||
|
maxEntryId);
|
||||||
|
fetchedCount = statuses.size();
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
|
statuses =
|
||||||
|
feedEntryStatusDAO.findBySubscriptions(
|
||||||
|
user,
|
||||||
|
subs,
|
||||||
|
unreadOnly,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
limit,
|
||||||
|
order,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
minEntryId,
|
||||||
|
maxEntryId);
|
||||||
|
fetchedCount = statuses.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String nextContinuation = null;
|
||||||
|
if (fetchedCount >= limit && fetchedCount > 0) {
|
||||||
|
if (streamId.type() == GoogleReaderStreamId.Type.STARRED) {
|
||||||
|
nextContinuation = String.valueOf(offset + limit);
|
||||||
|
} else if (!statuses.isEmpty()) {
|
||||||
|
nextContinuation = String.valueOf(statuses.getLast().getEntry().getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PagedStatuses(statuses, nextContinuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoogleReaderModel.StreamItem mapItem(FeedEntryStatus status) {
|
||||||
|
FeedEntry entry = status.getEntry();
|
||||||
|
FeedSubscription sub = status.getSubscription();
|
||||||
|
|
||||||
|
GoogleReaderModel.StreamItem item = new GoogleReaderModel.StreamItem();
|
||||||
|
item.setId(formatItemId(entry.getId()));
|
||||||
|
|
||||||
|
Instant published = entry.getPublished() != null ? entry.getPublished() : Instant.EPOCH;
|
||||||
|
item.setPublished(published.getEpochSecond());
|
||||||
|
item.setUpdated(published.getEpochSecond());
|
||||||
|
item.setCrawlTimeMsec(String.valueOf(entry.getInserted().toEpochMilli()));
|
||||||
|
item.setTimestampUsec(String.valueOf(published.toEpochMilli() * 1000));
|
||||||
|
item.setTitle(entry.getContent().getTitle());
|
||||||
|
item.setAuthor(entry.getContent().getAuthor());
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(entry.getUrl())) {
|
||||||
|
item.setCanonical(List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), null)));
|
||||||
|
item.setAlternate(
|
||||||
|
List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), "text/html")));
|
||||||
|
}
|
||||||
|
item.setSummary(
|
||||||
|
new GoogleReaderModel.Content(
|
||||||
|
Optional.ofNullable(entry.getContent().getContent()).orElse(""),
|
||||||
|
entry.getContent().getDirection() == FeedEntryContent.Direction.RTL
|
||||||
|
? "rtl"
|
||||||
|
: "ltr"));
|
||||||
|
|
||||||
|
List<String> categories = new ArrayList<>();
|
||||||
|
categories.add(GoogleReaderStreamId.READING_LIST);
|
||||||
|
if (status.isRead()) {
|
||||||
|
categories.add(GoogleReaderStreamId.READ);
|
||||||
|
}
|
||||||
|
if (status.isStarred()) {
|
||||||
|
categories.add(GoogleReaderStreamId.STARRED);
|
||||||
|
}
|
||||||
|
if (sub != null && sub.getCategory() != null) {
|
||||||
|
categories.add(GoogleReaderStreamId.label(sub.getCategory().getName()));
|
||||||
|
}
|
||||||
|
item.setCategories(categories);
|
||||||
|
|
||||||
|
if (sub != null) {
|
||||||
|
GoogleReaderModel.Origin origin = new GoogleReaderModel.Origin();
|
||||||
|
origin.setStreamId(GoogleReaderStreamId.feed(sub.getId()));
|
||||||
|
origin.setTitle(sub.getTitle());
|
||||||
|
origin.setHtmlUrl(sub.getFeed().getLink());
|
||||||
|
item.setOrigin(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoogleReaderModel.Subscription mapSubscription(FeedSubscription sub) {
|
||||||
|
GoogleReaderModel.Subscription s = new GoogleReaderModel.Subscription();
|
||||||
|
s.setId(GoogleReaderStreamId.feed(sub.getId()));
|
||||||
|
s.setTitle(sub.getTitle());
|
||||||
|
s.setUrl(sub.getFeed().getUrl());
|
||||||
|
s.setHtmlUrl(sub.getFeed().getLink());
|
||||||
|
s.setIconUrl(FeedUtils.getFaviconUrl(sub));
|
||||||
|
if (sub.getCategory() != null) {
|
||||||
|
s.getCategories()
|
||||||
|
.add(
|
||||||
|
new GoogleReaderModel.Category(
|
||||||
|
GoogleReaderStreamId.label(sub.getCategory().getName()),
|
||||||
|
sub.getCategory().getName()));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FeedCategory resolveLabelCategory(
|
||||||
|
User user, String labelParam, boolean createIfMissing) {
|
||||||
|
if (StringUtils.isBlank(labelParam)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(labelParam);
|
||||||
|
if (parsed.type() != GoogleReaderStreamId.Type.LABEL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = parsed.value();
|
||||||
|
FeedCategory category = findCategoryByName(user, name);
|
||||||
|
if (category == null && createIfMissing) {
|
||||||
|
category = new FeedCategory();
|
||||||
|
category.setUser(user);
|
||||||
|
category.setName(FeedUtils.truncate(name, 128));
|
||||||
|
feedCategoryDAO.persist(category);
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FeedCategory findCategoryByName(User user, String name) {
|
||||||
|
return feedCategoryDAO.findAll(user).stream()
|
||||||
|
.filter(c -> c.getName().equals(name))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String streamIdString(GoogleReaderStreamId parsed) {
|
||||||
|
return switch (parsed.type()) {
|
||||||
|
case FEED -> GoogleReaderStreamId.FEED_PREFIX + parsed.value();
|
||||||
|
case LABEL -> GoogleReaderStreamId.label(parsed.value());
|
||||||
|
case STARRED -> GoogleReaderStreamId.STARRED;
|
||||||
|
default -> GoogleReaderStreamId.READING_LIST;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String streamTitle(User user, GoogleReaderStreamId parsed) {
|
||||||
|
return switch (parsed.type()) {
|
||||||
|
case FEED -> {
|
||||||
|
Long subId = parsed.feedSubscriptionId();
|
||||||
|
FeedSubscription sub =
|
||||||
|
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||||
|
yield sub == null ? null : sub.getTitle();
|
||||||
|
}
|
||||||
|
case LABEL -> parsed.value();
|
||||||
|
case STARRED -> "Starred items";
|
||||||
|
default -> "reading list";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUnreadOnly(List<String> excludeTargets) {
|
||||||
|
return excludeTargets != null
|
||||||
|
&& excludeTargets.stream().anyMatch(t -> t.endsWith("/state/com.google/read"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsTag(List<String> tags, String suffix) {
|
||||||
|
return tags != null && tags.stream().anyMatch(t -> t.endsWith(suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatItemId(long id) {
|
||||||
|
// Google Reader's canonical long-form item id, e.g.
|
||||||
|
// "tag:google.com,2005:reader/item/00000000148b9369", where the suffix is the item's
|
||||||
|
// numeric id, represented as a zero-padded, 16-digit hex string
|
||||||
|
return "tag:google.com,2005:reader/item/" + String.format("%016x", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long parseItemId(String raw) {
|
||||||
|
if (StringUtils.isBlank(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int idx = raw.lastIndexOf('/');
|
||||||
|
if (raw.startsWith("tag:google.com") && idx != -1) {
|
||||||
|
try {
|
||||||
|
return Long.parseUnsignedLong(raw.substring(idx + 1), 16);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.valueOf(raw);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseOffset(String continuation) {
|
||||||
|
if (StringUtils.isBlank(continuation)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(continuation);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int clampLimit(Integer n) {
|
||||||
|
if (n == null || n <= 0) {
|
||||||
|
return DEFAULT_ITEM_COUNT;
|
||||||
|
}
|
||||||
|
return Math.min(n, MAX_ITEM_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toUsec(Instant instant) {
|
||||||
|
return instant == null ? "0" : String.valueOf(instant.toEpochMilli() * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validToken(User user, String token) {
|
||||||
|
// the edit token is a CSRF protection inherited from Google Reader's cookie-based
|
||||||
|
// authentication model. Since this API is authenticated via an api key header instead of
|
||||||
|
// cookies, that risk does not apply here. Some real-world clients (e.g. RSSGuard, unless
|
||||||
|
// configured as "Reedah"/"Miniflux") never call the "token" endpoint and always submit a
|
||||||
|
// blank token, so, blank tokens are accepted.
|
||||||
|
return StringUtils.isBlank(token) || computeEditToken(user).equals(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeEditToken(User user) {
|
||||||
|
return Digests.md5Hex(user.getApiKey() + ":" + user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private User currentUser() {
|
||||||
|
return authenticationContext.getCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PagedStatuses(List<FeedEntryStatus> statuses, String continuation) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.commafeed.frontend.resource.googlereader;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
public record GoogleReaderStreamId(Type type, String value) {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
ALL,
|
||||||
|
FEED,
|
||||||
|
LABEL,
|
||||||
|
STARRED
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String READING_LIST = "user/-/state/com.google/reading-list";
|
||||||
|
public static final String STARRED = "user/-/state/com.google/starred";
|
||||||
|
public static final String READ = "user/-/state/com.google/read";
|
||||||
|
public static final String LABEL_PREFIX = "user/-/label/";
|
||||||
|
public static final String FEED_PREFIX = "feed/";
|
||||||
|
|
||||||
|
public static final GoogleReaderStreamId ALL_STREAM = new GoogleReaderStreamId(Type.ALL, null);
|
||||||
|
public static final GoogleReaderStreamId STARRED_STREAM =
|
||||||
|
new GoogleReaderStreamId(Type.STARRED, null);
|
||||||
|
|
||||||
|
public static GoogleReaderStreamId parse(String raw) {
|
||||||
|
if (StringUtils.isBlank(raw) || "reading-list".equals(raw) || READING_LIST.equals(raw)) {
|
||||||
|
return ALL_STREAM;
|
||||||
|
}
|
||||||
|
if (STARRED.equals(raw)) {
|
||||||
|
return STARRED_STREAM;
|
||||||
|
}
|
||||||
|
if (raw.startsWith(LABEL_PREFIX)) {
|
||||||
|
return new GoogleReaderStreamId(Type.LABEL, raw.substring(LABEL_PREFIX.length()));
|
||||||
|
}
|
||||||
|
if (raw.startsWith(FEED_PREFIX)) {
|
||||||
|
return new GoogleReaderStreamId(Type.FEED, raw.substring(FEED_PREFIX.length()));
|
||||||
|
}
|
||||||
|
// unrecognized stream id, default to the whole reading list
|
||||||
|
return ALL_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String feed(long subscriptionId) {
|
||||||
|
return FEED_PREFIX + subscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String label(String categoryName) {
|
||||||
|
return LABEL_PREFIX + categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** extracts the subscription id out of a "feed/<id>" stream id, or null if invalid */
|
||||||
|
public Long feedSubscriptionId() {
|
||||||
|
if (type != Type.FEED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.valueOf(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,9 @@ import com.commafeed.backend.dao.UnitOfWork;
|
|||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
import com.commafeed.backend.service.UserService;
|
import com.commafeed.backend.service.UserService;
|
||||||
|
import com.commafeed.frontend.exception.CommaFeedApplicationException;
|
||||||
|
import com.commafeed.frontend.exception.CommaFeedExceptionType;
|
||||||
|
|
||||||
import io.quarkus.security.AuthenticationFailedException;
|
|
||||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||||
import io.quarkus.security.identity.IdentityProvider;
|
import io.quarkus.security.identity.IdentityProvider;
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
@@ -48,7 +49,8 @@ public class DatabaseUsernamePasswordIdentityProvider
|
|||||||
new String(
|
new String(
|
||||||
request.getPassword().getPassword())));
|
request.getPassword().getPassword())));
|
||||||
if (user.isEmpty()) {
|
if (user.isEmpty()) {
|
||||||
throw new AuthenticationFailedException("wrong username or password");
|
throw new CommaFeedApplicationException(
|
||||||
|
CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism {
|
public class ApiKeyAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uni<SecurityIdentity> authenticate(
|
public Uni<SecurityIdentity> authenticate(
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.commafeed.security.mechanism;
|
||||||
|
|
||||||
|
import io.quarkus.security.credential.TokenCredential;
|
||||||
|
import io.quarkus.security.identity.IdentityProviderManager;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.quarkus.security.identity.request.AuthenticationRequest;
|
||||||
|
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
|
||||||
|
import io.quarkus.vertx.http.runtime.security.ChallengeData;
|
||||||
|
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates requests to the Google Reader compatible API, using the "Authorization: GoogleLogin
|
||||||
|
* auth=<token>" header set by Google Reader API clients.
|
||||||
|
*
|
||||||
|
* <p>This mechanism only applies to requests under {@link #PATH_PREFIX}, so that a stray
|
||||||
|
* "Authorization: GoogleLogin ..." header sent to any other endpoint is not mistakenly accepted as
|
||||||
|
* an api key authentication.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class GoogleReaderAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||||
|
|
||||||
|
public static final String PATH_PREFIX = "/rest/googlereader";
|
||||||
|
|
||||||
|
private static final String AUTH_HEADER = "Authorization";
|
||||||
|
private static final String AUTH_PREFIX = "GoogleLogin auth=";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uni<SecurityIdentity> authenticate(
|
||||||
|
RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||||
|
if (!context.normalizedPath().startsWith(PATH_PREFIX)) {
|
||||||
|
return Uni.createFrom().optional(Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
String header = context.request().getHeader(AUTH_HEADER);
|
||||||
|
if (StringUtils.isBlank(header) || !header.startsWith(AUTH_PREFIX)) {
|
||||||
|
return Uni.createFrom().optional(Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = header.substring(AUTH_PREFIX.length()).trim();
|
||||||
|
int slashIndex = token.lastIndexOf('/');
|
||||||
|
String apiKey = slashIndex == -1 ? token : token.substring(slashIndex + 1);
|
||||||
|
if (StringUtils.isBlank(apiKey)) {
|
||||||
|
return Uni.createFrom().optional(Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenCredential credential = new TokenCredential(apiKey, "apiKey");
|
||||||
|
TokenAuthenticationRequest request = new TokenAuthenticationRequest(credential);
|
||||||
|
return identityProviderManager.authenticate(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uni<ChallengeData> getChallenge(RoutingContext context) {
|
||||||
|
return Uni.createFrom().optional(Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
|
||||||
|
return Set.of(TokenAuthenticationRequest.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ quarkus.http.port=8082
|
|||||||
quarkus.http.test-port=8085
|
quarkus.http.test-port=8085
|
||||||
quarkus.http.enable-compression=true
|
quarkus.http.enable-compression=true
|
||||||
quarkus.http.enable-decompression=true
|
quarkus.http.enable-decompression=true
|
||||||
|
|
||||||
# http cache
|
# http cache
|
||||||
quarkus.http.static-resources.max-age=P365d
|
quarkus.http.static-resources.max-age=P365d
|
||||||
## make sure the webapp is always up to date
|
## make sure the webapp is always up to date
|
||||||
@@ -15,7 +14,6 @@ quarkus.http.filter.openapi.matches=/openapi
|
|||||||
## make sure /next always redirects to the next unread entry
|
## make sure /next always redirects to the next unread entry
|
||||||
quarkus.http.filter.next.header."Cache-Control"=no-cache
|
quarkus.http.filter.next.header."Cache-Control"=no-cache
|
||||||
quarkus.http.filter.next.matches=/next
|
quarkus.http.filter.next.matches=/next
|
||||||
|
|
||||||
# security
|
# security
|
||||||
quarkus.http.auth.basic=true
|
quarkus.http.auth.basic=true
|
||||||
quarkus.http.auth.form.enabled=true
|
quarkus.http.auth.form.enabled=true
|
||||||
@@ -25,51 +23,42 @@ quarkus.http.auth.form.cookie-max-age=P30d
|
|||||||
quarkus.http.auth.form.landing-page=
|
quarkus.http.auth.form.landing-page=
|
||||||
quarkus.http.auth.form.login-page=
|
quarkus.http.auth.form.login-page=
|
||||||
quarkus.http.auth.form.error-page=
|
quarkus.http.auth.form.error-page=
|
||||||
|
|
||||||
# websocket
|
# websocket
|
||||||
quarkus.websocket.dispatch-to-worker=true
|
quarkus.websocket.dispatch-to-worker=true
|
||||||
|
|
||||||
# database
|
# database
|
||||||
quarkus.liquibase.change-log=migrations.xml
|
quarkus.liquibase.change-log=migrations.xml
|
||||||
quarkus.liquibase.migrate-at-start=true
|
quarkus.liquibase.migrate-at-start=true
|
||||||
|
|
||||||
# openapi
|
# openapi
|
||||||
quarkus.smallrye-openapi.path=/openapi
|
quarkus.smallrye-openapi.path=/openapi
|
||||||
quarkus.swagger-ui.path=/api-documentation
|
quarkus.swagger-ui.path=/api-documentation
|
||||||
quarkus.swagger-ui.always-include=true
|
quarkus.swagger-ui.always-include=true
|
||||||
|
|
||||||
# shutdown
|
# shutdown
|
||||||
quarkus.shutdown.timeout=5s
|
quarkus.shutdown.timeout=5s
|
||||||
|
|
||||||
# native
|
# native
|
||||||
quarkus.native.march=compatibility
|
quarkus.native.march=compatibility
|
||||||
quarkus.native.add-all-charsets=true
|
quarkus.native.add-all-charsets=true
|
||||||
# fix for https://github.com/Athou/commafeed/issues/1795
|
# fix for https://github.com/Athou/commafeed/issues/1795
|
||||||
quarkus.native.additional-build-args=-H:PageSize=65536
|
quarkus.native.additional-build-args=-H:PageSize=65536
|
||||||
|
|
||||||
|
|
||||||
# dev profile overrides
|
# dev profile overrides
|
||||||
%dev.quarkus.http.port=8083
|
%dev.quarkus.http.port=8083
|
||||||
%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890
|
%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890
|
||||||
%dev.quarkus.log.category."com.commafeed".level=DEBUG
|
%dev.quarkus.log.category."com.commafeed".level=DEBUG
|
||||||
# %dev.quarkus.hibernate-orm.log.sql=true
|
# %dev.quarkus.hibernate-orm.log.sql=true
|
||||||
%dev.commafeed.users.create-demo-account=true
|
%dev.commafeed.users.create-demo-account=true
|
||||||
|
|
||||||
|
|
||||||
# test profile overrides
|
# test profile overrides
|
||||||
%test.quarkus.log.category."org.mockserver".level=WARN
|
%test.quarkus.log.level=WARN
|
||||||
%test.quarkus.log.category."liquibase".level=WARN
|
%test.quarkus.log.category."org.mockserver".level=ERROR
|
||||||
%test.commafeed.shutdown-timeout=100ms
|
%test.commafeed.shutdown-timeout=100ms
|
||||||
%test.commafeed.users.create-demo-account=true
|
%test.commafeed.users.create-demo-account=true
|
||||||
%test.commafeed.users.allow-registrations=true
|
%test.commafeed.users.allow-registrations=true
|
||||||
%test.commafeed.password-recovery-enabled=true
|
%test.commafeed.password-recovery-enabled=true
|
||||||
|
%test.commafeed.password-recovery-public-base-url=https://commafeed.example.com
|
||||||
%test.commafeed.http-client.cache.enabled=false
|
%test.commafeed.http-client.cache.enabled=false
|
||||||
%test.commafeed.http-client.block-local-addresses=false
|
%test.commafeed.http-client.block-local-addresses=false
|
||||||
%test.commafeed.database.cleanup.entries-max-age=0
|
%test.commafeed.database.cleanup.entries-max-age=0
|
||||||
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
|
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
|
||||||
## GitHub actions runners are very slow, we need to increase this timeout to avoid intermittent test failures
|
## GitHub actions runners are very slow, we need to increase this timeout to avoid intermittent test failures
|
||||||
%test.commafeed.feed-refresh.filtering-expression-evaluation-timeout=5s
|
%test.commafeed.feed-refresh.filtering-expression-evaluation-timeout=5s
|
||||||
|
|
||||||
# prod profile overrides
|
# prod profile overrides
|
||||||
%prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE
|
%prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE
|
||||||
%prod.quarkus.datasource.username=sa
|
%prod.quarkus.datasource.username=sa
|
||||||
|
|||||||
@@ -547,5 +547,47 @@ class HttpGetterTest {
|
|||||||
Assertions.assertThrows(
|
Assertions.assertThrows(
|
||||||
UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]"));
|
UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nat64() {
|
||||||
|
// 64:ff9b::/96 embeds the IPv4 address in the lower 32 bits (RFC 6052)
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[64:ff9b::a9fe:a9fe]")); // 169.254.169.254
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[64:ff9b::a00:1]")); // 10.0.0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sixToFour() {
|
||||||
|
// 2002::/16 embeds the IPv4 address in bits 16-47 (RFC 3056)
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[2002:a9fe:a9fe::]")); // 169.254.169.254
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[2002:a00:1::]")); // 10.0.0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void teredo() {
|
||||||
|
// 2001:0000::/32 embeds the obfuscated (bitwise NOT) IPv4 address in the lower 32
|
||||||
|
// bits (RFC 4380)
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[2001:0:4136:e378:8000:63bf:f5ff:fffe]")); // 10.0.0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ipv4Compatible() {
|
||||||
|
// deprecated ::/96 IPv4-compatible addresses embed the IPv4 address in the lower 32
|
||||||
|
// bits
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class,
|
||||||
|
() -> getter.get("http://[::a9fe:a9fe]")); // 169.254.169.254
|
||||||
|
Assertions.assertThrows(
|
||||||
|
UnknownHostException.class, () -> getter.get("http://[::a00:1]")); // 10.0.0.1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class AuthentificationIT {
|
|||||||
page.navigate(getLoginPageUrl());
|
page.navigate(getLoginPageUrl());
|
||||||
PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password");
|
PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password");
|
||||||
PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT))
|
PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT))
|
||||||
.containsText("wrong username or password");
|
.containsText("Wrong username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.commafeed.integration;
|
package com.commafeed.integration;
|
||||||
|
|
||||||
|
import com.commafeed.ExceptionMappers.CommaFeedApplicationError;
|
||||||
import com.commafeed.ExceptionMappers.UnauthorizedResponse;
|
import com.commafeed.ExceptionMappers.UnauthorizedResponse;
|
||||||
import com.commafeed.TestConstants;
|
import com.commafeed.TestConstants;
|
||||||
|
import com.commafeed.frontend.exception.CommaFeedExceptionType;
|
||||||
import com.commafeed.frontend.model.Entries;
|
import com.commafeed.frontend.model.Entries;
|
||||||
import com.commafeed.frontend.model.UserModel;
|
import com.commafeed.frontend.model.UserModel;
|
||||||
import com.commafeed.frontend.model.request.MarkRequest;
|
import com.commafeed.frontend.model.request.MarkRequest;
|
||||||
@@ -57,6 +59,27 @@ class SecurityIT extends BaseIT {
|
|||||||
.statusCode(HttpStatus.SC_OK);
|
.statusCode(HttpStatus.SC_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void formLoginWrongPassword() {
|
||||||
|
CommaFeedApplicationError error =
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.formParams(
|
||||||
|
"j_username",
|
||||||
|
TestConstants.ADMIN_USERNAME,
|
||||||
|
"j_password",
|
||||||
|
"wrong-password")
|
||||||
|
.post("j_security_check")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_UNAUTHORIZED)
|
||||||
|
.extract()
|
||||||
|
.as(CommaFeedApplicationError.class);
|
||||||
|
|
||||||
|
Assertions.assertEquals(CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD, error.type());
|
||||||
|
Assertions.assertEquals("wrong username or password", error.message());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void basicAuthLogin() {
|
void basicAuthLogin() {
|
||||||
RestAssured.given()
|
RestAssured.given()
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ class DatabaseCleaningIT extends BaseIT {
|
|||||||
private void starEntry(String entryId, Long subscriptionId) {
|
private void starEntry(String entryId, Long subscriptionId) {
|
||||||
StarRequest starRequest = new StarRequest();
|
StarRequest starRequest = new StarRequest();
|
||||||
starRequest.setId(entryId);
|
starRequest.setId(entryId);
|
||||||
starRequest.setFeedId(subscriptionId);
|
|
||||||
starRequest.setStarred(true);
|
starRequest.setStarred(true);
|
||||||
RestAssured.given()
|
RestAssured.given()
|
||||||
.body(starRequest)
|
.body(starRequest)
|
||||||
@@ -58,7 +57,6 @@ class DatabaseCleaningIT extends BaseIT {
|
|||||||
private void unstarEntry(String entryId, Long subscriptionId) {
|
private void unstarEntry(String entryId, Long subscriptionId) {
|
||||||
StarRequest starRequest = new StarRequest();
|
StarRequest starRequest = new StarRequest();
|
||||||
starRequest.setId(entryId);
|
starRequest.setId(entryId);
|
||||||
starRequest.setFeedId(subscriptionId);
|
|
||||||
starRequest.setStarred(false);
|
starRequest.setStarred(false);
|
||||||
RestAssured.given()
|
RestAssured.given()
|
||||||
.body(starRequest)
|
.body(starRequest)
|
||||||
|
|||||||
@@ -240,7 +240,6 @@ class CategoryIT extends BaseIT {
|
|||||||
|
|
||||||
StarRequest starRequest = new StarRequest();
|
StarRequest starRequest = new StarRequest();
|
||||||
starRequest.setId(entry.getId());
|
starRequest.setId(entry.getId());
|
||||||
starRequest.setFeedId(subscriptionId);
|
|
||||||
starRequest.setStarred(true);
|
starRequest.setStarred(true);
|
||||||
RestAssured.given()
|
RestAssured.given()
|
||||||
.body(starRequest)
|
.body(starRequest)
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ class FeverIT extends BaseIT {
|
|||||||
|
|
||||||
StarRequest starRequest = new StarRequest();
|
StarRequest starRequest = new StarRequest();
|
||||||
starRequest.setId(entry.getId());
|
starRequest.setId(entry.getId());
|
||||||
starRequest.setFeedId(subscriptionId);
|
|
||||||
starRequest.setStarred(true);
|
starRequest.setStarred(true);
|
||||||
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star");
|
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
package com.commafeed.integration.rest;
|
||||||
|
|
||||||
|
import com.commafeed.TestConstants;
|
||||||
|
import com.commafeed.frontend.model.Entry;
|
||||||
|
import com.commafeed.frontend.model.UserModel;
|
||||||
|
import com.commafeed.frontend.model.request.ProfileModificationRequest;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.ItemRefs;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.StreamContents;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.SubscriptionList;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.TagList;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UnreadCounts;
|
||||||
|
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UserInfo;
|
||||||
|
import com.commafeed.integration.BaseIT;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class GoogleReaderIT extends BaseIT {
|
||||||
|
|
||||||
|
private static final String BASE = "rest/googlereader";
|
||||||
|
|
||||||
|
@Getter private String apiKey;
|
||||||
|
private GoogleReaderClient client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
|
||||||
|
RestAssured.authentication =
|
||||||
|
RestAssured.preemptive()
|
||||||
|
.basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
|
||||||
|
|
||||||
|
// create api key
|
||||||
|
ProfileModificationRequest req = new ProfileModificationRequest();
|
||||||
|
req.setCurrentPassword(TestConstants.ADMIN_PASSWORD);
|
||||||
|
req.setNewApiKey(true);
|
||||||
|
RestAssured.given()
|
||||||
|
.body(req)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.post("rest/user/profile")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_OK);
|
||||||
|
|
||||||
|
// retrieve api key
|
||||||
|
UserModel user =
|
||||||
|
RestAssured.given()
|
||||||
|
.get("rest/user/profile")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_OK)
|
||||||
|
.extract()
|
||||||
|
.as(UserModel.class);
|
||||||
|
this.apiKey = user.getApiKey();
|
||||||
|
this.client = new GoogleReaderClient(clientLogin(TestConstants.ADMIN_USERNAME, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clientLogin(String email, String password) {
|
||||||
|
String body =
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.formParam("Email", email)
|
||||||
|
.formParam("Passwd", password)
|
||||||
|
.post(BASE + "/accounts/ClientLogin")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_OK)
|
||||||
|
.extract()
|
||||||
|
.asString();
|
||||||
|
|
||||||
|
Map<String, String> values =
|
||||||
|
Arrays.stream(body.split("\n"))
|
||||||
|
.map(line -> line.split("=", 2))
|
||||||
|
.collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
|
||||||
|
return values.get("Auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidClientLogin() {
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.formParam("Email", TestConstants.ADMIN_USERNAME)
|
||||||
|
.formParam("Passwd", "invalid-key")
|
||||||
|
.post(BASE + "/accounts/ClientLogin")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validClientLogin() {
|
||||||
|
String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey);
|
||||||
|
Assertions.assertNotNull(token);
|
||||||
|
Assertions.assertTrue(token.endsWith("/" + apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidAuthToken() {
|
||||||
|
Response response =
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.header("Authorization", "GoogleLogin auth=invalid/invalid-key")
|
||||||
|
.get(BASE + "/reader/api/0/user-info");
|
||||||
|
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void authTokenOnlyAppliesToGoogleReaderEndpoints() {
|
||||||
|
String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey);
|
||||||
|
Response response =
|
||||||
|
RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.header("Authorization", "GoogleLogin auth=" + token)
|
||||||
|
.get("rest/user/profile");
|
||||||
|
Assertions.assertNotEquals(HttpStatus.SC_OK, response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void userInfo() {
|
||||||
|
UserInfo info = client.get(BASE + "/reader/api/0/user-info").as(UserInfo.class);
|
||||||
|
Assertions.assertEquals(TestConstants.ADMIN_USERNAME, info.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subscriptionList() {
|
||||||
|
subscribe(getFeedUrl());
|
||||||
|
SubscriptionList list =
|
||||||
|
client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
|
||||||
|
Assertions.assertEquals(1, list.getSubscriptions().size());
|
||||||
|
Assertions.assertEquals(
|
||||||
|
"my title for this feed", list.getSubscriptions().getFirst().getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tagList() {
|
||||||
|
createCategory("test-category");
|
||||||
|
TagList tagList = client.get(BASE + "/reader/api/0/tag/list").as(TagList.class);
|
||||||
|
Assertions.assertTrue(
|
||||||
|
tagList.getTags().stream()
|
||||||
|
.anyMatch(t -> t.getId().equals("user/-/label/test-category")));
|
||||||
|
Assertions.assertTrue(
|
||||||
|
tagList.getTags().stream()
|
||||||
|
.anyMatch(t -> t.getId().equals("user/-/state/com.google/starred")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unreadCount() {
|
||||||
|
subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
UnreadCounts counts =
|
||||||
|
client.get(BASE + "/reader/api/0/unread-count").as(UnreadCounts.class);
|
||||||
|
Assertions.assertTrue(
|
||||||
|
counts.getUnreadCounts().stream()
|
||||||
|
.anyMatch(
|
||||||
|
c ->
|
||||||
|
c.getId().equals("user/-/state/com.google/reading-list")
|
||||||
|
&& c.getCount() == 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streamContents() {
|
||||||
|
subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
StreamContents contents =
|
||||||
|
client.get(BASE + "/reader/api/0/stream/contents/reading-list")
|
||||||
|
.as(StreamContents.class);
|
||||||
|
Assertions.assertEquals(2, contents.getItems().size());
|
||||||
|
Assertions.assertEquals("Item 2", contents.getItems().getFirst().getTitle());
|
||||||
|
Assertions.assertTrue(
|
||||||
|
contents.getItems()
|
||||||
|
.getFirst()
|
||||||
|
.getId()
|
||||||
|
.matches("tag:google\\.com,2005:reader/item/[0-9a-f]{16}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streamItemIds() {
|
||||||
|
subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
ItemRefs refs =
|
||||||
|
client.get(
|
||||||
|
BASE
|
||||||
|
+ "/reader/api/0/stream/items/ids?s=user/-/state/com.google/reading-list")
|
||||||
|
.as(ItemRefs.class);
|
||||||
|
Assertions.assertEquals(2, refs.getItemRefs().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void editTagMarkRead() {
|
||||||
|
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
|
||||||
|
|
||||||
|
String token = client.get(BASE + "/reader/api/0/token").asString();
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/edit-tag",
|
||||||
|
Map.of("i", entry.getId(), "a", "user/-/state/com.google/read", "T", token));
|
||||||
|
|
||||||
|
Assertions.assertEquals(
|
||||||
|
1,
|
||||||
|
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
|
||||||
|
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/edit-tag",
|
||||||
|
Map.of("i", entry.getId(), "r", "user/-/state/com.google/read", "T", token));
|
||||||
|
|
||||||
|
Assertions.assertEquals(
|
||||||
|
0,
|
||||||
|
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void editTagMarkReadWithoutToken() {
|
||||||
|
// some real-world Google Reader API clients never fetch the edit token and always submit
|
||||||
|
// a blank one, this must still be accepted
|
||||||
|
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
|
||||||
|
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/edit-tag",
|
||||||
|
Map.of("i", entry.getId(), "a", "user/-/state/com.google/read"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(
|
||||||
|
1,
|
||||||
|
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void editTagStar() {
|
||||||
|
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
|
||||||
|
|
||||||
|
String token = client.get(BASE + "/reader/api/0/token").asString();
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/edit-tag",
|
||||||
|
Map.of("i", entry.getId(), "a", "user/-/state/com.google/starred", "T", token));
|
||||||
|
|
||||||
|
Assertions.assertEquals(
|
||||||
|
1,
|
||||||
|
getFeedEntries(subscriptionId).getEntries().stream()
|
||||||
|
.filter(Entry::isStarred)
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAllAsRead() {
|
||||||
|
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
|
||||||
|
String token = client.get(BASE + "/reader/api/0/token").asString();
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/mark-all-as-read",
|
||||||
|
Map.of("s", "user/-/state/com.google/reading-list", "T", token));
|
||||||
|
|
||||||
|
Assertions.assertTrue(
|
||||||
|
getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subscribeAndUnsubscribe() {
|
||||||
|
String token = client.get(BASE + "/reader/api/0/token").asString();
|
||||||
|
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/subscription/edit",
|
||||||
|
Map.of(
|
||||||
|
"ac",
|
||||||
|
"subscribe",
|
||||||
|
"s",
|
||||||
|
"feed/" + getFeedUrl(),
|
||||||
|
"t",
|
||||||
|
"new subscription",
|
||||||
|
"T",
|
||||||
|
token));
|
||||||
|
|
||||||
|
SubscriptionList list =
|
||||||
|
client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
|
||||||
|
Assertions.assertEquals(1, list.getSubscriptions().size());
|
||||||
|
String feedStreamId = list.getSubscriptions().getFirst().getId();
|
||||||
|
|
||||||
|
client.postForm(
|
||||||
|
BASE + "/reader/api/0/subscription/edit",
|
||||||
|
Map.of("ac", "unsubscribe", "s", feedStreamId, "T", token));
|
||||||
|
|
||||||
|
list = client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
|
||||||
|
Assertions.assertTrue(list.getSubscriptions().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GoogleReaderClient(String token) {
|
||||||
|
|
||||||
|
private RequestSpecification given() {
|
||||||
|
return RestAssured.given()
|
||||||
|
.auth()
|
||||||
|
.none()
|
||||||
|
.header("Authorization", "GoogleLogin auth=" + token);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response get(String path) {
|
||||||
|
return given().get(path).then().statusCode(HttpStatus.SC_OK).extract().response();
|
||||||
|
}
|
||||||
|
|
||||||
|
void postForm(String path, Map<String, ?> form) {
|
||||||
|
RequestSpecification spec = given();
|
||||||
|
form.forEach(spec::formParam);
|
||||||
|
spec.post(path).then().statusCode(HttpStatus.SC_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ class UserIT extends BaseIT {
|
|||||||
RestAssured.given()
|
RestAssured.given()
|
||||||
.body(req)
|
.body(req)
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
|
.header("Host", "malicious.url.com")
|
||||||
.post("rest/user/passwordReset")
|
.post("rest/user/passwordReset")
|
||||||
.then()
|
.then()
|
||||||
.statusCode(200);
|
.statusCode(200);
|
||||||
@@ -64,6 +65,7 @@ class UserIT extends BaseIT {
|
|||||||
Assertions.assertTrue(
|
Assertions.assertTrue(
|
||||||
message.getHtml()
|
message.getHtml()
|
||||||
.startsWith("You asked for password recovery for account 'admin'"));
|
.startsWith("You asked for password recovery for account 'admin'"));
|
||||||
|
Assertions.assertTrue(message.getHtml().contains("https://commafeed.example.com"));
|
||||||
Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst());
|
Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst());
|
||||||
|
|
||||||
Element a = Jsoup.parse(message.getHtml()).select("a").getFirst();
|
Element a = Jsoup.parse(message.getHtml()).select("a").getFirst();
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.2.1</version>
|
<version>7.3.2</version>
|
||||||
<name>CommaFeed</name>
|
<name>CommaFeed</name>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user