Compare commits

..

288 Commits

Author SHA1 Message Date
Jérémie Panzer
f5f77a80a3 Merge pull request #2341 from Athou/renovate/docker-login-action-4.x
Update docker/login-action action to v4.7.0
2026-10-23 20:36:02 +02:00
renovate[bot]
1d1099492e Update docker/login-action action to v4.7.0 2026-10-23 18:01:41 +00:00
Jérémie Panzer
ccc2564929 Merge pull request #2340 from Athou/renovate/com.ibm.icu-icu4j-79.x
Update dependency com.ibm.icu:icu4j to v79
2026-10-23 08:55:25 +02:00
renovate[bot]
084574ffd4 Update dependency com.ibm.icu:icu4j to v79 2026-10-22 20:43:24 +00:00
Athou
6a6978c491 release 7.4.1 2026-10-22 17:03:04 +02:00
Athou
0399375288 don't mark as read entries that the user explicitly marked as unread (#2337) 2026-10-22 09:04:57 +02:00
renovate[bot]
394346fbc1 Update dependency npm to v12.2.1 2026-10-22 01:00:28 +00:00
renovate[bot]
0feec109bd Update Node.js to v24.22.1 2026-10-21 12:22:26 +00:00
Jérémie Panzer
fdb5c9b536 Merge pull request #2339 from Athou/renovate/org.mock-server-mockserver-junit-jupiter-no-dependencies-9.x
Update dependency org.mock-server:mockserver-junit-jupiter-no-dependencies to v9.1.0
2026-10-21 14:21:44 +02:00
renovate[bot]
f7ad8aed60 Update dependency org.mock-server:mockserver-junit-jupiter-no-dependencies to v9.1.0 2026-10-21 11:48:07 +00:00
renovate[bot]
4621d7e79f Update debian:13.7 Docker digest to 4c25275 2026-10-21 05:25:57 +00:00
Jérémie Panzer
b64a49cfb0 Merge pull request #2338 from Athou/renovate/patch-mantine-monorepo
Update mantine monorepo to v9.7.3
2026-10-21 07:25:22 +02:00
renovate[bot]
aaddfbaf40 Update mantine monorepo to v9.7.3 2026-10-20 20:33:54 +00:00
Athou
994da5e1ee don't mark as read if we are still on the same entry (#2337) 2026-10-20 16:51:42 +02:00
Athou
811e282e28 mockserver already uses jackson3 2026-10-20 13:34:06 +02:00
Jérémie Panzer
892fcf8168 Merge pull request #2336 from Athou/renovate/linguijs-monorepo
Update linguijs monorepo to v6.10.0
2026-10-20 13:15:59 +02:00
renovate[bot]
032ee9243f Update linguijs monorepo to v6.10.0 2026-10-20 10:46:22 +00:00
renovate[bot]
30cdf34d56 Update quarkus.version to v3.40.1.1 2026-10-20 10:45:55 +00:00
Athou
fd52caa3fd use responseTimeout as the global timeout for the request 2026-10-20 08:47:59 +02:00
Athou
9788712808 add a global failsafe timeout to the feed fetching client 2026-10-20 08:28:15 +02:00
Jérémie Panzer
af9d086f05 Merge pull request #2335 from agross/add-oci-source-label
Add OCI source label for Renovate release-note discovery
2026-10-19 11:31:42 +02:00
Alexander Groß
fc7190868e Add OCI source label to container images 2026-10-19 10:35:18 +02:00
Athou
d3c1892c0b release 7.4.0 2026-10-19 09:07:03 +02:00
renovate[bot]
1fb4cdf3ca Lock file maintenance 2026-10-19 01:55:55 +00:00
renovate[bot]
7928183036 Update dependency vitest to v5.0.5 2026-10-18 16:41:22 +00:00
renovate[bot]
ffa276df9e Update dependency jsdom to v30.1.3 2026-10-18 13:36:44 +00:00
renovate[bot]
9f05e66e9d Update react monorepo to v19.3.1 2026-10-17 20:52:26 +00:00
renovate[bot]
8ba90ded85 Update ibm-semeru-runtimes:open-jdk-25.0.4.15-jre Docker digest to 630c738 2026-10-17 04:47:05 +00:00
renovate[bot]
4e5e2f06d3 Update dependency @testing-library/user-event to v14.6.8 2026-10-16 10:19:13 +00:00
Athou
72396254d2 Revert "prevent brotli4j from being on the classpath and extract its files in /tmp"
This reverts commit f9313006ba.
2026-10-16 12:18:09 +02:00
Athou
f9313006ba prevent brotli4j from being on the classpath and extract its files in /tmp 2026-10-16 12:05:56 +02:00
Jérémie Panzer
c894d701ff Merge pull request #2333 from Athou/renovate/react-querybuilder
Update react-querybuilder to v8.25.0
2026-10-16 06:12:26 +02:00
renovate[bot]
e9d0466018 Update react-querybuilder to v8.25.0 2026-10-16 02:45:03 +00:00
renovate[bot]
28b4f61f20 Update dependency vitest to v5.0.4 2026-10-15 11:14:46 +00:00
Athou
ab6b3b225d Revert "add support for zstd and brotli compression" because they use native libraries that need to be extracted in a temp folder which we want to avoid 2026-10-15 08:09:18 +02:00
Athou
ffb4a43e38 log what happens when we fail to compress 2026-10-15 03:07:48 +02:00
Jérémie Panzer
0653e2dc97 Merge pull request #2332 from Athou/renovate/node-24.x
Update Node.js to v24.22.0
2026-10-14 21:25:17 +02:00
renovate[bot]
912e03d2ce Update Node.js to v24.22.0 2026-10-14 19:05:35 +00:00
renovate[bot]
a8df765cda Update mantine monorepo to v9.7.2 2026-10-14 10:08:31 +00:00
Athou
b865b3ff84 add support for brotli compression 2026-10-14 07:37:31 +02:00
Athou
69c1e8a5b8 add support for zstd compression 2026-10-14 07:35:49 +02:00
Athou
c1522f1005 babel is no longer required for lingui 2026-10-14 02:49:30 +02:00
Athou
8144d24b06 Merge branch 'renovate/quarkus.version' 2026-10-14 02:46:27 +02:00
Athou
4c2e2efbb5 downgrade mockserver because 8.0.0 uses jackson 3 2026-10-14 02:46:09 +02:00
Jérémie Panzer
ad452c9a7a Merge pull request #2328 from Athou/renovate/mantine-monorepo
Update mantine monorepo to v9.7.1
2026-10-13 15:39:35 +02:00
Jérémie Panzer
a43da1304d Merge pull request #2325 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to v0.57.0
2026-10-13 15:39:28 +02:00
renovate[bot]
c2da59a92f Update quarkus.version to v3.40.1 2026-10-13 12:58:27 +00:00
renovate[bot]
a89166d1c4 Update mantine monorepo to v9.7.1 2026-10-13 12:58:16 +00:00
renovate[bot]
45039593bf Update dependency monaco-editor to v0.57.0 2026-10-13 12:58:02 +00:00
Athou
d56c35cdfb Merge remote-tracking branch 'origin/renovate/npm-12.x' 2026-10-13 14:56:19 +02:00
Athou
73836c6800 Merge remote-tracking branch 'origin/renovate/linguijs-monorepo' 2026-10-13 14:56:13 +02:00
Athou
f9a0440db8 Merge remote-tracking branch 'origin/renovate/graalvm-setup-graalvm-1.6.x' 2026-10-13 14:55:51 +02:00
Athou
6e7a8c1ef6 harden iframe protocols just in case 2026-10-13 13:24:42 +02:00
Athou
44147cc062 add more default headers (#2315) 2026-10-13 13:06:52 +02:00
renovate[bot]
352b756e22 Update linguijs monorepo to v6.9.1 2026-10-13 10:34:42 +00:00
renovate[bot]
a5d9eb757f Update dependency npm to v12.2.0 2026-10-13 10:34:20 +00:00
renovate[bot]
a19a73d675 Update graalvm/setup-graalvm action to v1.6.7 2026-10-13 10:34:13 +00:00
Athou
4a57f87284 Merge remote-tracking branch 'origin/renovate/patch-react-querybuilder' 2026-10-13 12:28:49 +02:00
Athou
b523fe4c58 Merge remote-tracking branch 'origin/renovate/patch-quarkus.version' 2026-10-13 12:28:44 +02:00
Athou
c208450abc Merge remote-tracking branch 'origin/renovate/patch-mantine-monorepo' 2026-10-13 12:28:39 +02:00
Athou
b73ab2a781 Merge remote-tracking branch 'origin/renovate/ibm-semeru-runtimes-25.0.x' 2026-10-13 12:28:34 +02:00
Athou
ec679cc23c Merge remote-tracking branch 'origin/renovate/patch-vitest-monorepo' 2026-10-13 12:28:22 +02:00
Athou
1dbbcc4d21 Merge remote-tracking branch 'origin/renovate/vite-8.3.x' 2026-10-13 12:28:17 +02:00
renovate[bot]
1573b61584 Update react-querybuilder to v8.24.4 2026-10-13 09:54:18 +00:00
renovate[bot]
1ac8cc7674 Update quarkus.version to v3.39.5 2026-10-13 09:54:10 +00:00
renovate[bot]
793db4e9b6 Update mantine monorepo to v9.6.3 2026-10-13 09:54:03 +00:00
renovate[bot]
12863e557a Update ibm-semeru-runtimes Docker tag to open-jdk-25.0.4.15-jre 2026-10-13 09:53:54 +00:00
renovate[bot]
d999fc1e06 Update dependency vitest to v5.0.3 2026-10-13 09:53:42 +00:00
renovate[bot]
6c288d160c Update dependency vite to v8.3.4 2026-10-13 09:53:32 +00:00
renovate[bot]
57d5dd8b20 Update dependency org.apache.httpcomponents.client5:httpclient5 to v5.6.5 2026-10-13 09:53:11 +00:00
Athou
0a8c0e0c7f don't stream the response at all if it exceeds the max size 2026-10-13 11:51:47 +02:00
Athou
f72bd4cc5c cancel requests if interrupted 2026-10-13 11:51:47 +02:00
Athou
26cce1faac we only need the response headers 2026-10-13 11:51:47 +02:00
Athou
3101b4f423 fix build 2026-10-13 11:51:47 +02:00
Jérémie Panzer
e8e30faf3e Merge pull request #2324 from Athou/renovate/maven-3.x
Update dependency maven to v3.10.0
2026-10-13 11:48:59 +02:00
Athou
3f6158fc47 add support for HTTP/2 (#2317) 2026-10-13 08:40:28 +02:00
renovate[bot]
3465645b70 Update dependency maven to v3.10.0 2026-10-13 04:41:08 +00:00
Jérémie Panzer
2dd397757d Merge pull request #2323 from Athou/renovate/com.puppycrawl.tools-checkstyle-14.x
Update dependency com.puppycrawl.tools:checkstyle to v14.3.0
2026-10-13 06:38:58 +02:00
renovate[bot]
5496419bb4 Update dependency com.puppycrawl.tools:checkstyle to v14.3.0 2026-10-13 02:18:41 +00:00
Jérémie Panzer
f1b950c38e Merge pull request #2321 from Athou/renovate/reduxjs-toolkit-2.x
Update dependency @reduxjs/toolkit to v2.13.0
2026-10-13 04:16:43 +02:00
renovate[bot]
4a3e97ad52 Update dependency @reduxjs/toolkit to v2.13.0 2026-10-12 19:20:48 +00:00
renovate[bot]
9fe17996b4 Update dependency jsdom to v30.1.2 2026-10-12 19:19:36 +00:00
Jérémie Panzer
c7a2f87148 Merge pull request #2320 from Athou/renovate/actions-setup-node-7.x
Update actions/setup-node action to v7.1.0
2026-10-12 21:18:01 +02:00
renovate[bot]
1125dd3fe7 Update actions/setup-node action to v7.1.0 2026-10-12 17:44:03 +00:00
renovate[bot]
adf48aee44 Update dependency @vitejs/plugin-react to v6.1.2 2026-10-12 15:41:38 +00:00
renovate[bot]
534d8a333e Update actions/upload-artifact action to v7.0.2 (#2319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 15:41:01 +00:00
renovate[bot]
03fc91c19c Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.4 2026-10-12 12:06:18 +00:00
renovate[bot]
d2429dada2 Update actions/download-artifact action to v8.0.2 2026-10-12 10:37:39 +00:00
renovate[bot]
0fae323647 Update debian:13.7 Docker digest to 913f670 2026-10-12 08:33:18 +00:00
renovate[bot]
fae58d56be Update dependency io.quarkiverse.playwright:quarkus-playwright to v2.3.10 2026-10-04 19:08:08 +00:00
renovate[bot]
fc56d5e7cb Update dependency @biomejs/biome to v2.5.15 2026-10-04 11:38:21 +00:00
Jérémie Panzer
04c85acae0 Merge pull request #2316 from Athou/renovate/querydsl.version
Update querydsl.version to v7.7
2026-09-25 07:52:23 +09:00
renovate[bot]
af2fd9bc09 Update querydsl.version to v7.7 2026-09-23 16:56:48 +00:00
renovate[bot]
d4ce2a213f Update dependency @babel/types to ^8.0.6 2026-09-22 00:13:21 +00:00
renovate[bot]
7d9ba805e4 Lock file maintenance 2026-09-21 01:27:17 +00:00
Jérémie Panzer
4c315ca3b8 Merge pull request #2314 from Athou/renovate/react-draggable-4.7.x
Update dependency react-draggable to v4.7.2
2026-09-20 22:56:52 +09:00
renovate[bot]
bcf812ef7a Update dependency react-draggable to v4.7.2 2026-09-20 13:21:11 +00:00
Jérémie Panzer
fd603aa76f Merge pull request #2313 from Athou/renovate/jsdom-30.x
Update dependency jsdom to v30.1.0
2026-09-20 22:20:07 +09:00
renovate[bot]
e34c08c803 Update dependency jsdom to v30.1.0 2026-09-20 04:40:09 +00:00
renovate[bot]
9252ef1531 Update dependency @biomejs/biome to v2.5.14 2026-09-19 13:24:53 +00:00
Jérémie Panzer
63153ca195 Merge pull request #2312 from Athou/renovate/debian-13.x
Update debian Docker tag to v13.7
2026-09-19 22:24:13 +09:00
Jérémie Panzer
a8c2f69251 Merge pull request #2311 from Athou/renovate/react-querybuilder
Update react-querybuilder to v8.24.0
2026-09-19 22:23:58 +09:00
Jérémie Panzer
91eaef05e1 Merge pull request #2310 from Athou/renovate/io.github.hakky54-ayza-for-apache5-10.x
Update dependency io.github.hakky54:ayza-for-apache5 to v10.1.0
2026-09-19 22:23:46 +09:00
renovate[bot]
4822642fd7 Update debian Docker tag to v13.7 2026-09-19 12:17:42 +00:00
renovate[bot]
a712d8f900 Update react-querybuilder to v8.24.0 2026-09-19 02:35:24 +00:00
renovate[bot]
86e507f93e Update dependency io.github.hakky54:ayza-for-apache5 to v10.1.0 2026-09-19 02:35:00 +00:00
renovate[bot]
86eba276c5 Update dependency vitest to v5.0.1 2026-09-19 02:34:54 +00:00
renovate[bot]
4f18a98a4a Update dependency react-router-dom to v7.18.4 2026-09-18 21:43:13 +00:00
renovate[bot]
547eff9f06 Update ibm-semeru-runtimes:open-jdk-25.0.4.10-jre Docker digest to a95258f 2026-09-18 06:56:02 +00:00
renovate[bot]
5bd1225d3f Update dependency com.google.protobuf:protobuf-java to v4.36.2 2026-09-17 23:33:37 +00:00
renovate[bot]
309a0aa94c Update quarkus.version to v3.39.4 2026-09-17 17:40:08 +00:00
renovate[bot]
f8286a6334 Update ibm-semeru-runtimes:open-jdk-25.0.4.10-jre Docker digest to 4498f13 2026-09-16 17:12:40 +00:00
renovate[bot]
916086d60e Update docker/setup-buildx-action action to v4.4.1 2026-09-16 12:28:22 +00:00
renovate[bot]
4fa0eb0a87 Update ibm-semeru-runtimes:open-jdk-25.0.4.10-jre Docker digest to 2b1e6d5 2026-09-16 11:03:21 +00:00
Athou
295eb57922 oxc-transform-react is not yet stable 2026-09-15 18:10:29 +02:00
Jérémie Panzer
2179cf57b6 Merge pull request #2308 from Athou/renovate/docker-setup-qemu-action-4.x
Update docker/setup-qemu-action action to v4.4.0
2026-09-15 18:10:20 +02:00
Jérémie Panzer
cca0f2d5db Merge pull request #2309 from Athou/renovate/org.mock-server-mockserver-junit-jupiter-8.x
Update dependency org.mock-server:mockserver-junit-jupiter to v8
2026-09-15 18:10:17 +02:00
renovate[bot]
d3c3ffb88d Update dependency org.mock-server:mockserver-junit-jupiter to v8 2026-09-15 15:41:27 +00:00
renovate[bot]
10c7196f9f Update docker/setup-qemu-action action to v4.4.0 2026-09-15 15:41:22 +00:00
Jérémie Panzer
254def6152 Merge pull request #2307 from Athou/renovate/docker-setup-buildx-action-4.x
Update docker/setup-buildx-action action to v4.4.0
2026-09-15 17:40:40 +02:00
Jérémie Panzer
09bae2565c Merge pull request #2306 from Athou/renovate/docker-build-push-action-7.x
Update docker/build-push-action action to v7.4.0
2026-09-15 17:40:37 +02:00
renovate[bot]
5572918750 Update docker/setup-buildx-action action to v4.4.0 2026-09-15 13:53:58 +00:00
renovate[bot]
4e91db09a6 Update docker/build-push-action action to v7.4.0 2026-09-15 13:53:52 +00:00
renovate[bot]
bbaee20097 Update dependency yaml to v2.9.1 2026-09-14 20:59:56 +00:00
Jérémie Panzer
7554e2b03e Merge pull request #2305 from Athou/renovate/linguijs-monorepo
Update linguijs monorepo to v6.7.0
2026-09-14 17:58:35 +02:00
renovate[bot]
3baac1138f Update linguijs monorepo to v6.7.0 2026-09-14 15:30:16 +00:00
renovate[bot]
8815cef66c Lock file maintenance 2026-09-14 04:36:54 +00:00
renovate[bot]
afa836c6df Update dependency @babel/types to ^8.0.5 2026-09-14 02:45:25 +00:00
renovate[bot]
bf513508e4 Update quarkus.version to v3.39.3 2026-09-13 19:21:03 +00:00
renovate[bot]
a0423f301d Update mantine monorepo to v9.6.1 2026-09-13 16:28:15 +00:00
renovate[bot]
302fa8f4dd Update dependency org.codehaus.mojo:properties-maven-plugin to v1.3.1 2026-09-13 15:27:55 +00:00
Jérémie Panzer
4a75a22a6c Merge pull request #2304 from Athou/renovate/vite-8.x
Update dependency vite to v8.3.0
2026-09-13 17:27:27 +02:00
renovate[bot]
c7d5cf1cc2 Update dependency vite to v8.3.0 2026-09-13 13:45:56 +00:00
renovate[bot]
e031ca3ef4 Update dependency @biomejs/biome to v2.5.13 2026-09-13 13:45:19 +00:00
renovate[bot]
8e0e926a68 Update dependency org.codehaus.mojo:exec-maven-plugin to v3.6.4 2026-09-13 09:44:51 +00:00
renovate[bot]
d4ed634c4e Update dependency @rolldown/plugin-babel to v0.2.4 2026-09-13 06:48:14 +00:00
Jérémie Panzer
18582f1a2d Merge pull request #2303 from Athou/renovate/react-monorepo
Update react monorepo to v19.3.0
2026-09-13 08:46:42 +02:00
Jérémie Panzer
3688b128e4 Merge pull request #2302 from Athou/renovate/node-24.x
Update Node.js to v24.21.0
2026-09-13 08:46:00 +02:00
renovate[bot]
4718b75f89 Update react monorepo to v19.3.0 2026-09-13 03:24:38 +00:00
renovate[bot]
aa7f8f79a5 Update Node.js to v24.21.0 2026-09-13 03:24:30 +00:00
renovate[bot]
c794a6aaf8 Update ibm-semeru-runtimes:open-jdk-25.0.4.10-jre Docker digest to c9f7958 2026-09-13 03:23:33 +00:00
renovate[bot]
0786a45995 Lock file maintenance 2026-09-07 01:08:40 +00:00
renovate[bot]
66fde65550 Update dependency @types/react-dom to v19.2.7 2026-09-06 15:08:44 +00:00
Athou
caf56aca94 speed up tests by reusing the jsdom env since we don't rely on global state 2026-09-06 16:33:14 +02:00
Jérémie Panzer
06581692e6 Merge pull request #2301 from Athou/renovate/major-vitest-monorepo
Update dependency vitest to v5
2026-09-06 16:33:06 +02:00
Athou
ca2fbd9fdd fix build 2026-09-06 16:26:04 +02:00
renovate[bot]
81ce6c9950 Update dependency vitest to v5 2026-09-06 13:27:15 +00:00
renovate[bot]
31b67f2217 Update dependency @biomejs/biome to v2.5.12 2026-09-06 09:25:21 +00:00
renovate[bot]
0ee76d39b2 Update dependency @testing-library/user-event to v14.6.7 2026-09-05 02:42:43 +00:00
renovate[bot]
6a3f4816d5 Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.2 2026-09-04 19:13:01 +00:00
Jérémie Panzer
1365754d62 Merge pull request #2299 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.6.0
2026-09-04 10:08:52 +02:00
Athou
ac4c98bead maven-failsafe-plugin 3.6.0 introduced skipITs 2026-09-04 08:21:57 +02:00
renovate[bot]
74306719ca Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.6.0 2026-09-04 05:18:08 +00:00
Jérémie Panzer
cc028dd37f Merge pull request #2300 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.6.0
2026-09-04 06:20:05 +02:00
Jérémie Panzer
43184fff6d Merge pull request #2298 from Athou/renovate/mantine-monorepo
Update mantine monorepo to v9.6.0
2026-09-04 06:19:59 +02:00
renovate[bot]
d41f94f254 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.6.0 2026-09-04 01:52:42 +00:00
renovate[bot]
fb741a4e54 Update mantine monorepo to v9.6.0 2026-09-03 17:39:13 +00:00
renovate[bot]
4319dfd0fe Update quarkus.version to v3.39.2 2026-09-03 13:57:00 +00:00
Jérémie Panzer
a06a0e893c Merge pull request #2297 from Athou/renovate/org.apache.maven.plugins-maven-compiler-plugin-3.x
Update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.16.0
2026-09-03 07:13:35 +02:00
renovate[bot]
8517092a88 Update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.16.0 2026-09-03 02:03:10 +00:00
renovate[bot]
52e79dfaa7 Update graalvm/setup-graalvm action to v1.6.6 2026-09-02 17:42:08 +00:00
Athou
7fb19fd032 use exact version 2026-09-02 19:41:10 +02:00
Athou
6344657940 speed up client compilation by using oxc-transform-react as the react compiler 2026-09-02 19:17:03 +02:00
renovate[bot]
3f01a99b66 Update dependency org.projectlombok:lombok to v1.18.48 2026-09-02 06:05:10 +00:00
renovate[bot]
69d7350e89 Update actions/deploy-pages action to v5.0.1 2026-09-02 03:42:53 +00:00
renovate[bot]
9ee502f437 Update dependency io.github.git-commit-id:git-commit-id-maven-plugin to v10.0.1 2026-09-01 20:30:15 +00:00
Jérémie Panzer
b8b876535b Merge pull request #2295 from Athou/renovate/graalvm-setup-graalvm-1.6.x
Update graalvm/setup-graalvm action to v1.6.5
2026-09-01 22:29:25 +02:00
renovate[bot]
816edefe25 Update graalvm/setup-graalvm action to v1.6.5 2026-09-01 12:21:17 +00:00
Jérémie Panzer
c523ff608e Merge pull request #2294 from Athou/renovate/docker-setup-qemu-action-4.x
Update docker/setup-qemu-action action to v4.3.0
2026-09-01 14:20:18 +02:00
renovate[bot]
663f14c439 Update docker/setup-qemu-action action to v4.3.0 2026-09-01 11:32:29 +00:00
renovate[bot]
20c6ef6702 Update ibm-semeru-runtimes Docker tag to open-jdk-25.0.4.10-jre 2026-09-01 02:28:14 +00:00
renovate[bot]
91241f045f Update dependency com.google.protobuf:protobuf-java to v4.36.1 2026-08-31 22:47:46 +00:00
renovate[bot]
82e5c54cdc Update dependency react-router-dom to v7.18.3 2026-08-31 17:11:13 +00:00
Jérémie Panzer
4b17102e5a Merge pull request #2293 from dcelasun/chore/host-docs
Document Quarkus config for reverse proxies
2026-08-31 19:10:17 +02:00
D. Can Celasun
bc58324a56 Document Quarkus config for reverse proxies
Recent Quarkus versions started enforcing this. Let's document it so
users aren't caught unaware.
2026-08-31 17:14:45 +01:00
renovate[bot]
b43bcf244a Update dependency @vitejs/plugin-react to v6.1.1 2026-08-31 06:03:56 +00:00
renovate[bot]
befe1a0bdc Lock file maintenance 2026-08-31 03:01:31 +00:00
renovate[bot]
1aae5433f3 Update dependency @biomejs/biome to v2.5.11 2026-08-30 21:52:34 +00:00
renovate[bot]
afeb12686b Update dependency @testing-library/react to v16.3.3 2026-08-30 19:32:18 +00:00
Jérémie Panzer
75f4b94b26 Merge pull request #2290 from Athou/renovate/com.puppycrawl.tools-checkstyle-14.x
Update dependency com.puppycrawl.tools:checkstyle to v14.1.0
2026-08-30 21:31:18 +02:00
renovate[bot]
4a867904ad Update dependency com.puppycrawl.tools:checkstyle to v14.1.0 2026-08-30 17:54:42 +00:00
Jérémie Panzer
249b161243 Merge pull request #2288 from Athou/renovate/axios-1.x
Update dependency axios to v1.20.0
2026-08-29 11:35:03 +02:00
renovate[bot]
a709a3268a Update dependency axios to v1.20.0 2026-08-29 08:57:39 +00:00
renovate[bot]
50b8a06f63 Update dependency io.dropwizard.metrics:metrics-json to v4.2.40 2026-08-28 21:11:04 +00:00
renovate[bot]
29e6338b41 Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.1 2026-08-27 21:03:55 +00:00
renovate[bot]
82e9fd7f2e Update dependency @types/react-dom to v19.2.5 2026-08-27 01:04:14 +00:00
renovate[bot]
8eb41eec53 Update dependency io.github.hakky54:ayza-for-apache5 to v10.0.7 2026-08-26 19:26:48 +00:00
Jérémie Panzer
07f96019b1 Merge pull request #2285 from Athou/renovate/node-24.x
Update Node.js to v24.20.0
2026-08-26 21:25:29 +02:00
Jérémie Panzer
1a0d4cb8cd Merge pull request #2286 from Athou/renovate/quarkus.version
Update quarkus.version to v3.39.1
2026-08-26 21:25:26 +02:00
renovate[bot]
5ae34047ee Update quarkus.version to v3.39.1 2026-08-26 18:03:22 +00:00
renovate[bot]
260181ec7c Update Node.js to v24.20.0 2026-08-26 18:03:11 +00:00
renovate[bot]
21bc826eda Update dependency org.jsoup:jsoup to v1.23.2 2026-08-26 02:52:35 +00:00
renovate[bot]
e1d9125a5c Update mantine monorepo to v9.5.2 2026-08-25 19:38:44 +00:00
renovate[bot]
8d1b836f59 Update dependency @testing-library/user-event to v14.6.6 2026-08-25 18:44:45 +00:00
renovate[bot]
bd50399483 Update dependency @biomejs/biome to v2.5.10 2026-08-25 17:00:04 +00:00
renovate[bot]
d641d25392 Update debian:13.6 Docker digest to f324c7f 2026-08-25 13:14:52 +00:00
Athou
a8c9f41fce disable the pluginTimings check because it prints false positives (fixes #2283) 2026-08-24 16:10:40 +02:00
Athou
2ff9d84584 decrease npm log level to prevent "npm notice" from logging as ERROR (fixes #2282) 2026-08-24 16:01:40 +02:00
Athou
11cd61cd44 nowrap on parent is actually enough 2026-08-24 08:21:54 +02:00
Athou
cf43988fae force the header in mobile mode if it is too large even if above the mobile breakpoint (fixed #2080) 2026-08-24 07:44:04 +02:00
renovate[bot]
4ef27e3027 Lock file maintenance 2026-08-24 01:10:53 +00:00
renovate[bot]
5ffabb387b Update dependency vite to v8.2.2 2026-08-23 06:46:05 +00:00
Jérémie Panzer
d78a45b7e5 Merge pull request #2281 from Athou/renovate/vitejs-plugin-react-6.x
Update dependency @vitejs/plugin-react to v6.1.0
2026-08-23 08:45:02 +02:00
renovate[bot]
bd5a9c382d Update dependency @vitejs/plugin-react to v6.1.0 2026-08-23 05:07:19 +00:00
renovate[bot]
e238e829ad Update react-querybuilder to v8.23.1 2026-08-22 18:52:47 +00:00
Athou
689c20d367 release 7.3.2 2026-08-22 08:23:13 +02:00
Athou
43588df2b4 prevent users from starring/tagging entries that are not theirs 2026-08-22 08:14:21 +02:00
renovate[bot]
d2aaddea33 Update dependency vitest to v4.1.11 2026-08-21 18:52:50 +00:00
Jérémie Panzer
de233f8ae5 Merge pull request #2280 from Athou/renovate/patch-testing-library-monorepo
Update dependency @testing-library/user-event to v14.6.5
2026-08-21 15:06:31 +02:00
renovate[bot]
3179e74dc7 Update dependency @testing-library/user-event to v14.6.5 2026-08-21 04:47:20 +00:00
renovate[bot]
6f5e9f5d49 Update dependency @biomejs/biome to v2.5.9 2026-08-21 00:00:07 +00:00
Jérémie Panzer
8a43fdf200 Merge pull request #2279 from Athou/renovate/protobuf-monorepo
Update dependency com.google.protobuf:protobuf-java to v4.36.0
2026-08-21 01:59:09 +02:00
renovate[bot]
69051e5bbb Update dependency com.google.protobuf:protobuf-java to v4.36.0 2026-08-20 18:54:00 +00:00
renovate[bot]
c648e7e0ec Update dependency dayjs to v1.11.23 2026-08-20 15:32:41 +00:00
renovate[bot]
c91cf376ad Update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre Docker digest to 524f0d0 2026-08-20 09:23:52 +00:00
Jérémie Panzer
f4e07597d0 Merge pull request #2278 from canoine/patch-11
Update fr/messages.po
2026-08-20 11:22:57 +02:00
CNE
64b64d3340 Update fr/messages.po
3 new fields
2026-08-20 10:47:45 +02:00
Jérémie Panzer
57ea87871e Merge pull request #2276 from Athou/renovate/querydsl.version
Update querydsl.version to v7.6
2026-08-20 09:06:03 +02:00
renovate[bot]
6154d80bb9 Update querydsl.version to v7.6 2026-08-20 06:17:16 +00:00
renovate[bot]
ad85745f85 Update quarkus.version to v3.38.3 2026-08-20 00:35:52 +00:00
renovate[bot]
41fc77322c Update dependency dayjs to v1.11.22 2026-08-19 20:44:22 +00:00
Jérémie Panzer
1b83c26417 Merge pull request #2274 from Athou/renovate/docker-setup-buildx-action-4.x
Update docker/setup-buildx-action action to v4.3.0
2026-08-19 22:43:38 +02:00
Athou
10ea5f0b93 move exception to its own package 2026-08-19 22:32:37 +02:00
Jérémie Panzer
032684de68 Merge pull request #2272 from n200534/fix/issue-2016-translate-backend-errors
Translate login authentication errors
2026-08-19 22:31:29 +02:00
n200534
3efec5ce58 Use typed backend errors for translations 2026-08-20 01:11:30 +05:30
renovate[bot]
4ed32964d9 Update docker/setup-buildx-action action to v4.3.0 2026-08-19 16:16:18 +00:00
renovate[bot]
05c1d0cc6e Update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre Docker digest to 27d8480 2026-08-18 22:09:48 +00:00
Jérémie Panzer
3c54924971 Merge pull request #2273 from Athou/renovate/com.puppycrawl.tools-checkstyle-14.x
Update dependency com.puppycrawl.tools:checkstyle to v14
2026-08-18 21:48:54 +02:00
renovate[bot]
e57dfc0e3f Update dependency com.puppycrawl.tools:checkstyle to v14 2026-08-18 17:49:50 +00:00
n200534
eff8bb16a0 Translate login authentication errors 2026-08-18 15:33:42 +05:30
Jérémie Panzer
87e681b71e Merge pull request #2271 from Athou/renovate/dev.cel-cel-0.x
Update dependency dev.cel:cel to v0.14.0
2026-08-18 07:13:13 +02:00
renovate[bot]
ac73ef4ea7 Update dependency dev.cel:cel to v0.14.0 2026-08-18 02:44:30 +00:00
Jérémie Panzer
20945e7443 Merge pull request #2270 from Athou/renovate/react-querybuilder
Update react-querybuilder to v8.23.0
2026-08-17 23:16:27 +02:00
Jérémie Panzer
b7b1278111 Merge pull request #2269 from Athou/renovate/org.mock-server-mockserver-junit-jupiter-7.x
Update dependency org.mock-server:mockserver-junit-jupiter to v7.6.0
2026-08-17 23:14:40 +02:00
renovate[bot]
6f9378404f Update react-querybuilder to v8.23.0 2026-08-17 20:36:06 +00:00
renovate[bot]
79787aca6e Update dependency org.mock-server:mockserver-junit-jupiter to v7.6.0 2026-08-17 20:35:51 +00:00
Athou
2d90ccf856 release 7.3.1 2026-08-17 22:32:32 +02:00
Athou
7f77cf0268 prevent Host header injection attacks 2026-08-17 22:27:46 +02:00
Jérémie Panzer
f3c390e0e0 Merge pull request #2267 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-3.x
Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.0
2026-08-17 08:49:07 +02:00
Jérémie Panzer
1fdc471197 Merge pull request #2266 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
Update dependency com.puppycrawl.tools:checkstyle to v13.11.0
2026-08-17 07:45:37 +02:00
renovate[bot]
ecf5d9e50e Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.0 2026-08-17 05:44:41 +00:00
renovate[bot]
97a49e8faa Lock file maintenance 2026-08-17 00:41:26 +00:00
renovate[bot]
f5a020cdc2 Update dependency com.puppycrawl.tools:checkstyle to v13.11.0 2026-08-17 00:40:24 +00:00
renovate[bot]
1f0507ee65 Update dependency @testing-library/user-event to v14.6.4 2026-08-15 01:00:48 +00:00
renovate[bot]
393c90f334 Update react-querybuilder to v8.22.5 2026-08-14 23:10:07 +00:00
renovate[bot]
03ecc2f49b Update dependency @biomejs/biome to v2.5.8 2026-08-14 09:15:31 +00:00
renovate[bot]
17743adce6 Update quarkus.version to v3.38.2 2026-08-13 17:05:44 +00:00
renovate[bot]
126dde103a Update dependency @testing-library/jest-dom to v7.0.1 2026-08-13 00:07:02 +00:00
renovate[bot]
14a25581b4 Update react-querybuilder to v8.22.4 2026-08-11 06:18:20 +00:00
Athou
c7c95efb3a release 7.3.0 2026-08-11 08:17:05 +02:00
Athou
4c1293d357 change the default value of blockLocalAddresses 2026-08-11 08:07:56 +02:00
Athou
76a92ea44c more SSRF protection regarding IPv6 2026-08-11 07:46:02 +02:00
Jérémie Panzer
ba454322db Merge pull request #2263 from Athou/renovate/org.apache.commons-commons-collections4-4.x
fix(deps): update dependency org.apache.commons:commons-collections4 to v4.6.0
2026-08-10 16:55:55 +02:00
renovate[bot]
a3b9f471e1 fix(deps): update dependency org.apache.commons:commons-collections4 to v4.6.0 2026-08-10 13:47:04 +00:00
Athou
d58ddc888e fix flaky tests 2026-08-10 09:45:00 +02:00
Athou
fe9837c45c fix typo 2026-08-10 09:30:31 +02:00
Athou
332d126bd7 reduce log verbosity during tests 2026-08-10 09:08:15 +02:00
Athou
0c66bf2695 speed up tests 2026-08-10 09:05:11 +02:00
Athou
4d6d07c98b remove warning 2026-08-10 09:02:07 +02:00
renovate[bot]
8f7d4bfa10 chore(deps): lock file maintenance 2026-08-10 05:05:39 +00:00
renovate[bot]
7b3f39001f fix(deps): update react-querybuilder to v8.22.3 2026-08-10 03:27:17 +00:00
Jérémie Panzer
b9630809a0 Merge pull request #2262 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.10.0
2026-08-10 05:26:27 +02:00
renovate[bot]
ce3244da5e chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.10.0 2026-08-09 21:51:02 +00:00
renovate[bot]
4067297d2a chore(deps): update dependency vite to v8.2.1 2026-08-09 17:40:42 +00:00
renovate[bot]
03c11fa181 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.6.4 2026-08-09 09:42:31 +00:00
renovate[bot]
3676e78eb9 fix(deps): update react-querybuilder to v8.22.2 2026-08-08 16:44:38 +00:00
Athou
1e08c4486f fix google reader api icon url 2026-08-08 16:06:47 +02:00
Athou
fd8bb60daa google reader api support 2026-08-08 10:22:58 +02:00
renovate[bot]
557d56ffc5 fix(deps): update react-querybuilder to v8.22.1 2026-08-08 04:30:28 +08:00
renovate[bot]
30f76a2349 chore(deps): update dependency @biomejs/biome to v2.5.7 2026-08-07 15:07:29 +00:00
renovate[bot]
cf0fa5e22e chore(deps): update dependency @testing-library/user-event to v14.6.3 2026-08-07 04:00:58 +00:00
Jérémie Panzer
f16becbd82 Merge pull request #2259 from Athou/renovate/graalvm-setup-graalvm-1.6.x
chore(deps): update graalvm/setup-graalvm action to v1.6.4
2026-08-07 06:00:37 +02:00
Jérémie Panzer
4855450ddc Merge pull request #2260 from Athou/renovate/react-querybuilder
fix(deps): update react-querybuilder to v8.22.0
2026-08-07 06:00:33 +02:00
Jérémie Panzer
0733aedce5 Merge pull request #2257 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2026-08-07 06:00:25 +02:00
renovate[bot]
399ad14d9b chore(deps): lock file maintenance 2026-08-07 02:59:35 +00:00
renovate[bot]
7881ea3c5c fix(deps): update react-querybuilder to v8.22.0 2026-08-07 02:58:54 +00:00
renovate[bot]
7e9de622da chore(deps): update graalvm/setup-graalvm action to v1.6.4 2026-08-07 02:58:46 +00:00
Athou
c6655078f2 fix build 2026-08-07 04:58:06 +02:00
Athou
b86cef9499 package-lock refresh 2026-08-06 21:47:31 +02:00
Athou
5375ab6e97 profile menu now works again on mobile when action buttons are at the bottom of the screen 2026-08-06 21:46:47 +02:00
renovate[bot]
75920a6473 fix(deps): update mantine monorepo to v9.5.1 2026-08-05 08:38:05 +00:00
renovate[bot]
f474e76866 chore(deps): update debian:13.6 docker digest to 34cd9e9 2026-08-05 04:50:23 +00:00
renovate[bot]
d0ae27c4ce fix(deps): update quarkus.version to v3.38.1 2026-08-04 17:07:29 +00:00
renovate[bot]
ab191dded6 chore(deps): update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre docker digest to 02888a2 2026-08-04 10:32:11 +00:00
renovate[bot]
950b571a9f chore(deps): update ibm-semeru-runtimes docker tag to open-jdk-25.0.4.0-jre 2026-08-04 04:59:45 +00:00
Jérémie Panzer
7a153ca69a Merge pull request #2258 from Athou/renovate/node-24.x
chore(deps): update node.js to v24.19.0
2026-08-03 19:08:30 +02:00
renovate[bot]
effd8946de chore(deps): update node.js to v24.19.0 2026-08-03 16:42:20 +00:00
Athou
ae3ad3745c move global System.setProperty calls in CommaFeedApplication 2026-08-03 09:15:23 +02:00
91 changed files with 4117 additions and 1320 deletions

View File

@@ -29,14 +29,14 @@ jobs:
# Setup
- name: Set up GraalVM
uses: graalvm/setup-graalvm@0def53c0fd8534bc13416c9469f5be45265824fd # v1.6.3
uses: graalvm/setup-graalvm@27b360dbb857524eaf0b4c4cef364d38e5b4f173 # v1.6.7
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
cache: "maven"
- name: Set up NodeJS cache
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
cache: "npm"
cache-dependency-path: 'commafeed-client/package-lock.json'
@@ -47,7 +47,7 @@ jobs:
# Build & Test
- name: Build with Maven
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipITs=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
# Build pages
- name: Create pages directory structure
@@ -73,14 +73,14 @@ jobs:
# Upload artifacts
- name: Upload cross-platform app
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
with:
name: commafeed-${{ matrix.database }}-jvm
path: commafeed-server/target/commafeed-*.zip
- name: Upload native executable
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner*
@@ -110,17 +110,17 @@ jobs:
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Install required packages
run: sudo apt-get install -y rename unzip
# Prepare artifacts
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: commafeed-${{ matrix.database }}-*
path: ./artifacts
@@ -141,7 +141,7 @@ jobs:
# Docker
- name: Login to Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
if: ${{ env.DOCKERHUB_USERNAME != '' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
## build but don't push for PRs and renovate
- name: Docker build - native
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
@@ -157,7 +157,7 @@ jobs:
platforms: linux/amd64,linux/arm64/v8
- name: Docker build - jvm
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
@@ -166,7 +166,7 @@ jobs:
## build and push tag
- name: Docker build and push tag - native
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -178,7 +178,7 @@ jobs:
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -191,7 +191,7 @@ jobs:
## build and push master
- name: Docker build and push master - native
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -201,7 +201,7 @@ jobs:
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -226,7 +226,7 @@ jobs:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: commafeed-*
path: ./artifacts
@@ -280,5 +280,5 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
- uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
id: deployment

View File

@@ -1,3 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

View File

@@ -1,9 +1,44 @@
# Changelog
## [7.4.1]
- Fix entries being marked as read when scrolling back to an entry that was explicitly marked as unread (#2337)
- Add a global failsafe timeout to the new HTTP/2 client to prevent it from hanging indefinitely
- Add OCI source label to container images (#2335)
## [7.4.0]
- Add support for HTTP/2 when fetching feeds (#2317)
- Send more default headers when fetching feeds, to avoid being rejected by some servers (#2315)
- Harden the content cleaning of feed entries by only allowing `http`/`https` protocols in `iframe` `src` attributes
- Fix the header in mobile mode being wrapped/cut off when too wide, even above the mobile breakpoint (#2080)
- Document how to configure Quarkus host validation when running CommaFeed behind a reverse proxy
## [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]
- 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]
@@ -13,26 +48,36 @@
- Small performance improvements to the feed refresh engine
- 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
- Strengthened the SSRF protection by also blocking ULA and CGNAT ranges
- Prevent DDOS attacks by filtering OPML files during imports
- Strengthened the SSRF protection by also blocking ULA and CGNAT ranges (GHSA-hgrr-mjfp-gmr6)
- Prevent DDOS attacks by filtering OPML files during imports (GHSA-83jp-rww3-57rr)
## [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)
- 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]
- 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.
- Added a per-feed setting for sending push notifications to ntfy, Gotify or Pushover when new feed entries are discovered (#1610)
- 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.
- 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)
- 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.
- When `commafeed.http-client.block-local-addresses` is enabled, SSRF is now also mitigated by blocking public websites redirecting to local ones.
- 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.
- 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]
- 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]
@@ -40,16 +85,21 @@
## [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)
- The profile menu is now closed when scrolling the page (#2019)
- The "disable pull to refresh" feature is now disabled by default (#2030)
## [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
- 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)
- 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
- 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
## [5.12.1]
@@ -79,15 +129,18 @@
## [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
- 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]
- 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)
## [5.8.0]
@@ -96,7 +149,8 @@
- 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)
- 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]
@@ -115,14 +169,22 @@
## [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)
- 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
- 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
- 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
- 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]
- 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
- Fix an issue with some labels not correctly internationalized
@@ -139,8 +201,10 @@
## [5.3.5]
- 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)
- 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)
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age
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]
@@ -167,19 +231,22 @@
- 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)
- 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)
## [5.1.1]
- 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]
- 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)
- 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
## [5.0.2]
@@ -194,9 +261,9 @@
## [5.0.0]
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 gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
0.3s) and very low memory footprint (< 50M).
the [announcement](https://github.com/Athou/commafeed/discussions/1517). The gist of it is that CommaFeed can now be
compiled to a native binary, resulting in blazing fast startup times (around 0.3s) and very low memory footprint (<
50M).
- CommaFeed now has a different package for each supported database.
- If you are deploying CommaFeed with a precompiled package, please
@@ -252,8 +319,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 two options in the settings to toggle those buttons
- 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 (
older than `keepStatusDays`) (#1303)
- 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)
## [4.3.3]
@@ -297,9 +364,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)
- 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)
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup.
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set
to 365 days
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup. The setting is
disabled by default for existing installations, except for the docker image where it is enabled and set to 365 days
- 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)
- the sidebar resizer is no longer shown in the middle of the screen on mobile
@@ -325,8 +391,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
request, reducing CPU usage
- updated UI library Mantine to 7.0, improving performance
- 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
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is recommended
(see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
- 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
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
@@ -568,7 +634,6 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
## [2.0.0]
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
consumption and better overall performances.
See the README on how to build CommaFeed from now on.
consumption and better overall performances. See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in

View File

@@ -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
- OPML import/export
- 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
- Push notifications when new articles are published
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
@@ -159,6 +159,28 @@ named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/download
This is the JVM used in
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.
### Listen on a single network interface
By default, CommaFeed listens on all interfaces. You can restrict it by setting `quarkus.http.host`.
Note that if you set it to a local name like `127.0.0.1` host validation is enabled automatically. This prevents
access if you're using a reverse proxy like Nginx. To fix, add your actual hostname to `allowed-hosts`:
```
quarkus.http.host=127.0.0.1
quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.allow-forwarded=true
quarkus.http.host-validation.allowed-hosts=commafeed.example.com
```
## Translation
Files for internationalization are

1
commafeed-client/.npmrc Normal file
View File

@@ -0,0 +1 @@
loglevel=warn

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

@@ -17,35 +17,35 @@
"dependencies": {
"@emotion/react": "11.14.0",
"@fontsource/open-sans": "5.3.0",
"@lingui/core": "6.6.0",
"@lingui/react": "6.6.0",
"@mantine/core": "9.5.0",
"@mantine/dates": "9.5.0",
"@mantine/form": "9.5.0",
"@mantine/hooks": "9.5.0",
"@mantine/modals": "9.5.0",
"@mantine/notifications": "9.5.0",
"@mantine/spotlight": "9.5.0",
"@lingui/core": "6.10.0",
"@lingui/react": "6.10.0",
"@mantine/core": "9.7.3",
"@mantine/dates": "9.7.3",
"@mantine/form": "9.7.3",
"@mantine/hooks": "9.7.3",
"@mantine/modals": "9.7.3",
"@mantine/notifications": "9.7.3",
"@mantine/spotlight": "9.7.3",
"@monaco-editor/react": "4.7.0",
"@react-querybuilder/mantine": "8.21.2",
"@reduxjs/toolkit": "2.12.0",
"@rolldown/plugin-babel": "0.2.3",
"axios": "1.19.0",
"dayjs": "1.11.21",
"@react-querybuilder/mantine": "8.25.0",
"@reduxjs/toolkit": "2.13.0",
"@rolldown/plugin-babel": "0.2.4",
"axios": "1.20.0",
"dayjs": "1.11.23",
"escape-string-regexp": "5.0.0",
"interweave": "13.1.1",
"monaco-editor": "0.56.0",
"monaco-editor": "0.57.0",
"mousetrap": "1.6.5",
"react": "19.2.8",
"react": "19.3.1",
"react-async-hook": "4.0.0",
"react-contexify": "6.0.0",
"react-dom": "19.2.8",
"react-draggable": "4.7.1",
"react-dom": "19.3.1",
"react-draggable": "4.7.2",
"react-icons": "5.7.0",
"react-infinite-scroller": "1.2.6",
"react-querybuilder": "8.21.2",
"react-querybuilder": "8.25.0",
"react-redux": "9.3.0",
"react-router-dom": "7.18.2",
"react-router-dom": "7.18.4",
"react-swipeable": "7.0.2",
"style-to-object": "2.0.2",
"throttle-debounce": "5.0.2",
@@ -54,31 +54,31 @@
"websocket-heartbeat-js": "1.1.3"
},
"devDependencies": {
"@biomejs/biome": "2.5.6",
"@lingui/babel-plugin-lingui-macro": "6.6.0",
"@lingui/cli": "6.6.0",
"@lingui/vite-plugin": "6.6.0",
"@testing-library/jest-dom": "7.0.0",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@babel/types": "^8.0.6",
"@biomejs/biome": "2.5.15",
"@lingui/cli": "6.10.0",
"@lingui/vite-plugin": "6.10.0",
"@testing-library/jest-dom": "7.0.1",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.8",
"@types/mousetrap": "1.6.15",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.4",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@types/react-infinite-scroller": "1.2.5",
"@types/throttle-debounce": "5.0.2",
"@types/tinycon": "0.6.7",
"@vitejs/plugin-react": "6.0.5",
"babel-plugin-react-compiler": "1.0.0",
"jsdom": "30.0.1",
"@vitejs/plugin-react": "6.1.2",
"babel-plugin-react-compiler": "^1.0.0",
"jsdom": "30.1.3",
"typescript": "7.0.2",
"vite": "8.2.0",
"vite": "8.3.4",
"vite-plugin-checker": "0.14.5",
"vitest": "4.1.10",
"yaml": "2.9.0"
"vitest": "5.0.5",
"yaml": "2.9.1"
},
"overrides": {
"react-infinite-scroller": {
"react": "19.2.8"
"react": "19.3.1"
}
}
}

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>7.2.1</version>
<version>7.4.1</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v24.18.1</node.version>
<node.version>v24.22.1</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>12.0.2</npm.version>
<npm.version>12.2.1</npm.version>
</properties>
<build>
@@ -110,7 +110,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<version>3.6.4</version>
<executions>
<execution>
<id>kill-biome</id>

View 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"])
})
})

View File

@@ -1,3 +1,5 @@
import { i18n, type MessageDescriptor } from "@lingui/core"
import { msg } from "@lingui/core/macro"
import axios, { type AxiosError } from "axios"
import type {
AddCategoryRequest,
@@ -6,6 +8,8 @@ import type {
Category,
CategoryModificationRequest,
CollapseRequest,
CommaFeedApplicationError,
CommaFeedExceptionType,
Entries,
FeedInfo,
FeedInfoRequest,
@@ -31,6 +35,10 @@ import type {
UserModel,
} 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 })
axiosInstance.interceptors.response.use(
response => response,
@@ -128,14 +136,25 @@ export const errorToStrings = (err: unknown) => {
let strings: string[] = []
if (axios.isAxiosError(err) && err.response) {
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]
if (isCommaFeedApplicationError(err)) {
strings.push(i18n._(applicationErrorMessages[err.response.data.type]))
} 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
}
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 }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
}

View File

@@ -116,6 +116,30 @@ describe("entries", () => {
expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
})
it("flags an entry marked as unread by the user until entries are reloaded", async () => {
vi.mocked(client.feed.getEntries).mockResolvedValue({
data: {
entries: [{ id: "3", read: true, markable: true } as Entry],
hasMore: false,
name: "my-feed",
errorCount: 0,
feedLink: "https://mysite.com/feed",
timestamp: 123,
ignoredReadStatus: false,
},
} as AxiosResponse<Entries>)
const store = configureStore({ reducer: reducers })
await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true }))
store.dispatch(markEntry({ entry: store.getState().entries.entries[0], read: false }))
expect(store.getState().entries.entries[0].keepUnread).toBe(true)
// reloading the entry list resets the flag
await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: false }))
expect(store.getState().entries.entries[0].keepUnread).toBeUndefined()
})
it("marks all entries as read", () => {
const store = configureStore({
reducer: reducers,

View File

@@ -10,7 +10,11 @@ export interface EntrySource {
id: string
}
export type ExpendableEntry = Entry & { expanded?: boolean }
export type ExpendableEntry = Entry & {
expanded?: boolean
/** set when the user explicitly marks the entry as unread, prevents it from being marked as read again by scrolling in expanded mode until the entry list is reloaded */
keepUnread?: boolean
}
interface EntriesState {
/** selected source */
@@ -71,11 +75,13 @@ export const entriesSlice = createSlice({
builder.addCase(markEntry.pending, (state, action) => {
for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
e.read = action.meta.arg.read
if (!action.meta.arg.read) e.keepUnread = true
}
})
builder.addCase(markMultipleEntries.pending, (state, action) => {
for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
e.read = action.meta.arg.read
if (!action.meta.arg.read) e.keepUnread = true
}
})
builder.addCase(markAllEntries.pending, (state, action) => {

View File

@@ -166,7 +166,6 @@ export const starEntry = createAppAsyncThunk(
(arg: { entry: Entry; starred: boolean }) => {
client.entry.star({
id: arg.entry.id,
feedId: +arg.entry.feedId,
starred: arg.starred,
})
},

View File

@@ -297,7 +297,6 @@ export interface LocalSettings {
export interface StarRequest {
id: string
feedId: number
starred: boolean
}
@@ -338,3 +337,10 @@ export interface AuthenticationError {
message: string
allowRegistrations: boolean
}
export type CommaFeedExceptionType = "WRONG_USERNAME_OR_PASSWORD"
export interface CommaFeedApplicationError {
type: CommaFeedExceptionType
message: string
}

View File

@@ -5,14 +5,15 @@ import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
import { Constants } from "@/app/constants"
import { useActionButton } from "@/hooks/useActionButton"
export type Mode = "auto" | "mobile" | "desktop"
interface ActionButtonProps {
icon: ReactNode
className?: string
label?: string | MessageDescriptor
onClick?: MouseEventHandler
variant?: ActionIconVariant & ButtonVariant
hideLabelOnDesktop?: boolean
showLabelOnMobile?: boolean
mode?: Mode
}
/**
@@ -25,7 +26,9 @@ export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props
const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
const variant = props.variant ?? "subtle"
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
const mode: Mode = props.mode ?? "auto"
const iconOnly = mode === "mobile" || (mode === "auto" && mobile)
return (
<Box ref={ref} className="cf-action-button">

View File

@@ -111,12 +111,13 @@ export function FeedEntries() {
const el = document.getElementById(Constants.dom.entryId(e))
return el && !Constants.layout.isTopVisible(el)
})
if (currentEntry) {
if (currentEntry && currentEntry.id !== selectedEntryId) {
dispatch(
selectEntry({
entry: currentEntry,
expand: false,
markAsRead: !!scrollMarks,
// don't mark as read entries that the user explicitly marked as unread
markAsRead: !!scrollMarks && !currentEntry.keepUnread,
scrollToEntry: false,
})
)
@@ -124,7 +125,7 @@ export function FeedEntries() {
})
window.addEventListener("scroll", listener)
return () => window.removeEventListener("scroll", listener)
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry, selectedEntryId])
useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap(

View File

@@ -2,7 +2,7 @@ import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { useEffect } from "react"
import { type ReactNode, useEffect } from "react"
import {
TbArrowDown,
TbArrowUp,
@@ -31,7 +31,7 @@ function HeaderDivider() {
return <Divider orientation="vertical" />
}
function HeaderToolbar(props: { children: React.ReactNode }) {
function HeaderToolbar(props: { children: ReactNode }) {
const { spacing } = useActionButton()
const mobile = useMobile("480px")
return mobile ? (
@@ -47,7 +47,7 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
{props.children}
</Box>
) : (
<Group gap={spacing} className="cf-toolbar">
<Group gap={spacing} wrap="nowrap" className="cf-toolbar">
{props.children}
</Group>
)

View File

@@ -124,7 +124,7 @@ export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
}
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.Dropdown>
{profile && <Menu.Label>{profile.name}</Menu.Label>}

View File

@@ -91,8 +91,8 @@ export function ProfileSettings() {
label={<Trans>API key</Trans>}
description={
<Trans>
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
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
</Trans>
}
readOnly
@@ -130,6 +130,22 @@ export function ProfileSettings() {
</Box>
</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 />
<PasswordInput

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -454,6 +458,14 @@ msgstr "انتقل إلى وثائق API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح الوصول إلى Fever API. استخدم النموذج الموجود في أسفل الصفحة لإنشاء مفتاح API جديد"
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 و Google Reader API. استخدم النموذج الموجود أسفل الصفحة لإنشاء مفتاح API جديد"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "مرحباً! يبدو أن هذه هي المرة الأولى التي تقوم فيها بتشغيل CommaFeed. يرجى إنشاء حساب مسؤول للبدء."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "أصفر"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit"
@@ -454,6 +458,14 @@ msgstr "Vés a la documentació de l'API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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."
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 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
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."
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
msgid "Yellow"
msgstr "Groc"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka"
@@ -454,6 +458,14 @@ msgstr "Přejděte na dokumentaci API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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."
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 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
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."
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
msgid "Yellow"
msgstr "Žlutá"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -454,6 +458,14 @@ msgstr "Ewch i'r ddogfennaeth API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Melyn"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentationen."
msgid "Goodies"
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
msgid "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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Gul"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element"
@@ -454,6 +458,14 @@ msgstr "Gehen Sie zur API-Dokumentation."
msgid "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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Gelb"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item"
@@ -454,6 +458,14 @@ msgstr "Go to the API documentation."
msgid "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
msgid "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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Yellow"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído"
@@ -455,6 +459,14 @@ msgstr "Ir a la documentación de la API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Amarillo"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -454,6 +458,14 @@ msgstr "به مستندات API بروید."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
msgstr "این کلید API شماست. می‌توان از آن برای برخی عملیات API فقط خواندنی استفاده کرد و دسترسی به Fever API را فراهم می‌کند. از فرم پایین صفحه برای تولید کلید جدید استفاده کنید"
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 و Google Reader API را می‌دهد. از فرم پایین صفحه برای ایجاد کلید API جدید استفاده کنید"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "خوش آمدید! به نظر می‌رسد این اولین بار است که CommaFeed را اجرا می‌کنید. لطفاً برای شروع یک حساب مدیر ایجاد کنید."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "زرد"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde"
@@ -454,6 +458,14 @@ msgstr "Siirry API-dokumentaatioon."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Keltainen"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu"
@@ -454,6 +458,14 @@ msgstr "Aller à la documentation de l'API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Jaune"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "Seguinte elemento non lido en CommaFeed"
@@ -455,6 +459,14 @@ msgstr "Ir á documentación da API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Amarelo"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem"
@@ -454,6 +458,14 @@ msgstr "Nyissa meg az API dokumentációját."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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. 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
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."
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
msgid "Yellow"
msgstr "Sárga"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -454,6 +458,14 @@ msgstr "Buka dokumentasi API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Kuning"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto"
@@ -454,6 +458,14 @@ msgstr "Vai alla documentazione dell'API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Giallo"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed 次の未読アイテム"
@@ -454,6 +458,14 @@ msgstr "APIドキュメントに移動します。"
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。"
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およびGoogle Reader APIへのアクセスを許可します。新しいAPIキーを生成するには、ページ下部のフォームを使用してください"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "ようこそ!CommaFeedを初めて実行するようです。開始するには管理者アカウントを作成してください。"
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "イエロー"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
@@ -454,6 +458,14 @@ msgstr "API 문서로 이동합니다."
msgid "Goodies"
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
msgid "Grape"
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>이었습니다."
#: 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"
msgstr "이것은 귀하의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오."
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 및 Google Reader API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "환영합니다! CommaFeed를 처음 실행하는 것 같습니다. 시작하려면 관리자 계정을 만드십시오."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "옐로우"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya"
@@ -454,6 +458,14 @@ msgstr "Pergi ke dokumentasi API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Kuning"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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ø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
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."
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
msgid "Yellow"
msgstr "Gul"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item"
@@ -454,6 +458,14 @@ msgstr "Ga naar de API-documentatie."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Geel"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste ulesne element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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ø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
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."
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
msgid "Yellow"
msgstr "Gul"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element"
@@ -454,6 +458,14 @@ msgstr "Przejdź do dokumentacji API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 i API Google Reader. Użyj formularza na dole strony, aby wygenerować nowy klucz API"
#: src/components/settings/DisplaySettings.tsx
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."
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
msgid "Yellow"
msgstr "Żółty"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido"
@@ -454,6 +458,14 @@ msgstr "Vá para a documentação da API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 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
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."
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
msgid "Yellow"
msgstr "Amarelo"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент"
@@ -454,6 +458,14 @@ msgstr "Перейдите к документации по API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever. Чтобы сгенерировать новый ключ API, воспользуйтесь формой в нижней части страницы"
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 Google Reader. Используйте форму внизу страницы, чтобы сгенерировать новый ключ API"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "Добро пожаловать! Похоже, вы запускаете CommaFeed в первый раз. Пожалуйста, создайте учетную запись администратора, чтобы начать работу."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "Желтый"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka"
@@ -454,6 +458,14 @@ msgstr "Prejdite na dokumentáciu API."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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."
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ť 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
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."
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
msgid "Yellow"
msgstr "Žltá"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt"
@@ -454,6 +458,14 @@ msgstr "Gå till API-dokumentationen."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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 "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
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."
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
msgid "Yellow"
msgstr "Gul"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe"
@@ -454,6 +458,14 @@ msgstr "API belgelerine gidin."
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
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"
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, 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
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."
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
msgid "Yellow"
msgstr "Sarı"

View File

@@ -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>."
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
msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目"
@@ -454,6 +458,14 @@ msgstr "API 文档"
msgid "Goodies"
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
msgid "Grape"
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>"
#: 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"
msgstr "这是您的API 密钥,它可以被用于Fever API的只读操作及访问授权。使用页面底部的表单生成一个新的API密钥。"
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 和 Google Reader API 的访问权限。使用页面底部的表单生成新的 API 密钥"
#: src/components/settings/DisplaySettings.tsx
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."
msgstr "欢迎!当前页仅当您第一次使用CommaFeed时出现,请创建一个管理员帐号以开始使用。"
#: src/app/client.ts
msgid "Wrong username or password"
msgstr "用户名或密码错误"
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
msgstr "黄"

View File

@@ -15,9 +15,10 @@ dayjs.extend(relativeTime)
dayjs.extend(duration)
const root = document.getElementById("root")
root &&
ReactDOM.createRoot(root).render(
<Provider store={store}>
<App />
</Provider>
)
if (!root) throw new Error("root element not found")
ReactDOM.createRoot(root).render(
<Provider store={store}>
<App />
</Provider>
)

View File

@@ -42,7 +42,7 @@ export function WelcomePage() {
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={async () => await login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile
mode="desktop"
/>
</Center>
)}
@@ -100,7 +100,7 @@ function Buttons() {
icon={<TbKey size={iconSize} />}
variant="outline"
onClick={async () => await dispatch(redirectToLogin())}
showLabelOnMobile
mode="desktop"
/>
{serverInfos?.allowRegistrations && (
<ActionButton
@@ -108,7 +108,7 @@ function Buttons() {
icon={<TbUserPlus size={iconSize} />}
variant="filled"
onClick={async () => await dispatch(redirectToRegistration())}
showLabelOnMobile
mode="desktop"
/>
)}
@@ -116,7 +116,7 @@ function Buttons() {
label={dark ? msg`Switch to light theme` : msg`Switch to dark theme`}
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
onClick={() => toggleColorScheme()}
hideLabelOnDesktop
mode="mobile"
/>
{isBrowserExtensionPopup && (
@@ -124,7 +124,7 @@ function Buttons() {
label={msg`Extension options`}
icon={<TbSettings size={iconSize} />}
onClick={() => openSettingsPage()}
hideLabelOnDesktop
mode="mobile"
/>
)}
</Group>

View File

@@ -227,7 +227,7 @@ export default function Layout(props: Readonly<LayoutProps>) {
)}
</OnMobile>
<OnDesktop>
<Group p="md">
<Group p="md" wrap="nowrap">
<Group justify="space-between" style={{ width: sidebarWidth - 16 }}>
<Box>
<LogoAndTitle />

View File

@@ -1,4 +1,4 @@
import "@testing-library/jest-dom"
import "@testing-library/jest-dom/vitest"
import { vi } from "vitest"
import { Constants } from "@/app/constants"

View File

@@ -7,11 +7,8 @@ import checker from "vite-plugin-checker"
export default defineConfig(() => ({
plugins: [
react(),
babel({
presets: [reactCompilerPreset()],
plugins: ["@lingui/babel-plugin-lingui-macro"],
}),
lingui(),
lingui({ macroTransform: true }),
babel({ presets: [reactCompilerPreset()] }),
checker({
// temporary disabled until TypeScript 7 exposes a stable api
// typescript: true,
@@ -44,6 +41,7 @@ export default defineConfig(() => ({
inconsistentCjsInterop: true,
},
test: {
isolate: false,
environment: "jsdom",
globals: true,
setupFiles: "./src/setupTests.ts",
@@ -51,6 +49,9 @@ export default defineConfig(() => ({
build: {
chunkSizeWarningLimit: 4000,
rolldownOptions: {
checks: {
pluginTimings: false,
},
output: {
codeSplitting: {
groups: [

View File

@@ -6,15 +6,15 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>7.2.1</version>
<version>7.4.1</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<packaging>quarkus</packaging>
<properties>
<quarkus.version>3.38.0</quarkus.version>
<querydsl.version>7.5</querydsl.version>
<quarkus.version>3.40.1.1</quarkus.version>
<querydsl.version>7.7</querydsl.version>
<rome.version>2.1.0</rome.version>
<!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
<google-java-format.version>1.36.1</google-java-format.version>
@@ -35,7 +35,7 @@
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.35.1</version>
<version>4.36.2</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -64,7 +64,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.3.0</version>
<version>1.3.1</version>
<executions>
<execution>
<goals>
@@ -116,7 +116,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<version>3.6.4</version>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
@@ -164,7 +164,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
<version>3.6.0</version>
<configuration>
<argLine>@{argLine}</argLine>
<systemPropertyVariables>
@@ -176,7 +176,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.6</version>
<version>3.6.0</version>
<executions>
<execution>
<goals>
@@ -221,7 +221,7 @@
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<version>10.0.0</version>
<version>10.0.1</version>
<executions>
<execution>
<goals>
@@ -245,7 +245,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.9.0</version>
<version>14.3.0</version>
</dependency>
</dependencies>
<executions>
@@ -274,7 +274,7 @@
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.9.0</version>
<version>3.10.4</version>
<?m2e ignore?>
<executions>
<execution>
@@ -302,14 +302,14 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>7.2.1</version>
<version>7.4.1</version>
</dependency>
<!-- compile-time processors -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.46</version>
<version>1.18.48</version>
</dependency>
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
@@ -322,6 +322,11 @@
<artifactId>quarkus-extension-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>nativeimage</artifactId>
<scope>provided</scope>
</dependency>
<!-- quarkus dependencies -->
<dependency>
@@ -368,7 +373,7 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>4.2.39</version>
<version>4.2.40</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
@@ -389,7 +394,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.5.0</version>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@@ -399,7 +404,7 @@
<dependency>
<groupId>dev.cel</groupId>
<artifactId>cel</artifactId>
<version>0.13.1</version>
<version>0.14.0</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
@@ -429,12 +434,12 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.23.1</version>
<version>1.23.2</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>78.3</version>
<version>79.1</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cssparser</groupId>
@@ -449,17 +454,12 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.6.3</version>
<version>5.6.5</version>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>ayza-for-apache5</artifactId>
<version>10.0.6</version>
</dependency>
<dependency>
<groupId>org.brotli</groupId>
<artifactId>dec</artifactId>
<version>0.1.2</version>
<version>10.1.0</version>
</dependency>
<dependency>
<groupId>com.github.seancfoley</groupId>
@@ -476,13 +476,13 @@
<dependency>
<groupId>io.quarkiverse.playwright</groupId>
<artifactId>quarkus-playwright</artifactId>
<version>2.3.8</version>
<version>2.3.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-junit-jupiter</artifactId>
<version>7.5.0</version>
<artifactId>mockserver-junit-jupiter-no-dependencies</artifactId>
<version>9.1.0</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -1,4 +1,6 @@
FROM ibm-semeru-runtimes:open-jdk-25.0.3.0-jre@sha256:43ec947fee9c4c2ed880620ae9e78911ac5eef66391e5a2c43f956dcc9000491
FROM ibm-semeru-runtimes:open-jdk-25.0.4.15-jre@sha256:630c738228597b4cd31d9823e479384729b03684bac23478b33fdbda13ae31b8
LABEL org.opencontainers.image.source="https://github.com/Athou/commafeed"
EXPOSE 8082
RUN mkdir -p /commafeed/data

View File

@@ -1,4 +1,6 @@
FROM debian:13.6@sha256:fac46bff2e02f51425b6e33b0e1169f55dfb053d83511ca28aa50c09fd5ed7a4
FROM debian:13.7@sha256:4c25275446ca371766b8b093c696782eabce06263fccacb8d5d813b3dc44b308
LABEL org.opencontainers.image.source="https://github.com/Athou/commafeed"
ARG TARGETARCH
EXPOSE 8082

View File

@@ -93,3 +93,12 @@ Tags are of the form `<version>-<database>[-jvm]` where:
- `master` (always points to the latest git commit)
- `<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.
## 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.

View File

@@ -14,6 +14,8 @@ import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemProperties;
@Slf4j
@Singleton
@RequiredArgsConstructor
@@ -26,6 +28,12 @@ public class CommaFeedApplication {
public void start(@Observes StartupEvent ev) {
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(
config.users().minimumPasswordLength());
@@ -33,6 +41,11 @@ public class CommaFeedApplication {
ImageProxyUrl.generateKey();
}
if (config.passwordRecoveryEnabled() && config.passwordRecoveryPublicBaseUrl().isEmpty()) {
throw new IllegalStateException(
"password recovery is enabled but no public base url is set");
}
feedRefreshEngine.start();
taskScheduler.start();
}

View File

@@ -49,6 +49,13 @@ public interface CommaFeedConfiguration {
@WithDefault("false")
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. */
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
* registrations open.
*/
@WithDefault("false")
@WithDefault("true")
boolean blockLocalAddresses();
/** HTTP client cache configuration */

View File

@@ -1,5 +1,7 @@
package com.commafeed;
import com.commafeed.frontend.exception.CommaFeedApplicationException;
import com.commafeed.frontend.exception.CommaFeedExceptionType;
import com.commafeed.security.CookieService;
import io.quarkus.runtime.annotations.RegisterForReflection;
@@ -26,6 +28,18 @@ public class ExceptionMappers {
private final CookieService cookieService;
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)
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
return RestResponse.status(
@@ -55,4 +69,7 @@ public class ExceptionMappers {
@RegisterForReflection
public record ValidationFailed(String message) {}
@RegisterForReflection
public record CommaFeedApplicationError(CommaFeedExceptionType type, String message) {}
}

View File

@@ -6,6 +6,8 @@ import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.quarkus.runtime.annotations.RegisterForReflection;
@@ -346,4 +348,22 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
com.rometools.opml.feed.synd.impl.ConverterForOPML10.class,
com.rometools.opml.feed.synd.impl.ConverterForOPML20.class,
})
public class NativeImageClasses {}
public class NativeImageClasses {
// disable zstd and brotli runtime detection because it causes native image failure
@TargetClass(className = "org.apache.hc.client5.http.impl.ZstdRuntime")
static final class ZstdRuntime {
@Substitute
public static boolean available() {
return false;
}
}
@TargetClass(className = "org.apache.hc.client5.http.impl.Brotli4jRuntime")
static final class Brotli4jRuntime {
@Substitute
public static boolean available() {
return false;
}
}
}

View File

@@ -7,6 +7,8 @@ import com.google.common.net.HttpHeaders;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressNetwork;
import inet.ipaddr.IPAddressString;
import inet.ipaddr.ipv4.IPv4Address;
import inet.ipaddr.ipv6.IPv6Address;
import jakarta.inject.Singleton;
@@ -17,27 +19,27 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.async.methods.InflatingAsyncDataConsumer;
import org.apache.hc.client5.http.async.methods.InflatingGzipDataConsumer;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.entity.DeflateInputStream;
import org.apache.hc.client5.http.entity.InputStreamFactory;
import org.apache.hc.client5.http.entity.compress.ContentCoding;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.brotli.dec.BrotliInputStream;
import java.net.InetAddress;
import java.net.URI;
@@ -46,7 +48,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedMap;
import java.util.zip.GZIPInputStream;
import java.util.function.UnaryOperator;
@Singleton
@RequiredArgsConstructor
@@ -58,8 +60,8 @@ public class HttpClientFactory {
private final CommaFeedConfiguration config;
private final CommaFeedVersion version;
public CloseableHttpClient newClient(int poolSize) {
PoolingHttpClientConnectionManager connectionManager =
public CloseableHttpAsyncClient newClient(int poolSize) {
PoolingAsyncClientConnectionManager connectionManager =
newConnectionManager(config, poolSize);
String userAgent =
config.httpClient()
@@ -72,42 +74,48 @@ public class HttpClientFactory {
return newClient(config, connectionManager, userAgent);
}
private CloseableHttpClient newClient(
private CloseableHttpAsyncClient newClient(
CommaFeedConfiguration config,
HttpClientConnectionManager connectionManager,
PoolingAsyncClientConnectionManager connectionManager,
String userAgent) {
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache"));
headers.add(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
headers.add(new BasicHeader(HttpHeaders.SEC_FETCH_MODE, "navigate"));
SequencedMap<String, InputStreamFactory> contentDecoderMap = new LinkedHashMap<>();
contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new);
contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
SequencedMap<String, UnaryOperator<AsyncDataConsumer>> contentDecoderMap =
new LinkedHashMap<>();
contentDecoderMap.put(
ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
contentDecoderMap.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new);
RedirectStrategy redirectStrategy =
config.httpClient().blockLocalAddresses()
? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER)
: new DefaultRedirectStrategy();
return HttpClientBuilder.create()
.disableConnectionState()
.useSystemProperties()
.disableAutomaticRetries()
.disableCookieManagement()
.setUserAgent(userAgent)
.setDefaultHeaders(headers)
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(
TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
.setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
.setRedirectStrategy(redirectStrategy)
.build();
CloseableHttpAsyncClient client =
HttpAsyncClients.custom()
.disableConnectionState()
.useSystemProperties()
.disableAutomaticRetries()
.disableCookieManagement()
.setUserAgent(userAgent)
.setDefaultHeaders(headers)
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(
TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
.setContentDecoderMap(new LinkedHashMap<>(contentDecoderMap))
.setRedirectStrategy(redirectStrategy)
.build();
client.start();
return client;
}
private PoolingHttpClientConnectionManager newConnectionManager(
private PoolingAsyncClientConnectionManager newConnectionManager(
CommaFeedConfiguration config, int poolSize) {
SSLFactory sslFactory =
SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
@@ -116,8 +124,8 @@ public class HttpClientFactory {
? new BlockLocalAddressesDnsResolver(DNS_RESOLVER)
: DNS_RESOLVER;
return PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
return PoolingAsyncClientConnectionManagerBuilder.create()
.setTlsStrategy(Apache5SslUtils.toTlsStrategy(sslFactory))
.setDefaultConnectionConfig(
ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
@@ -127,6 +135,7 @@ public class HttpClientFactory {
.build())
.setDefaultTlsConfig(
TlsConfig.custom()
.setVersionPolicy(HttpVersionPolicy.NEGOTIATE)
.setHandshakeTimeout(
Timeout.of(config.httpClient().sslHandshakeTimeout()))
.build())
@@ -137,8 +146,38 @@ public class HttpClientFactory {
}
private static boolean isLocalAddress(InetAddress address) {
IPAddress ip = new IPAddressNetwork.IPAddressGenerator().from(address);
return ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip);
return isLocalAddress(new IPAddressNetwork.IPAddressGenerator().from(address));
}
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 {

View File

@@ -6,7 +6,6 @@ import com.commafeed.CommaFeedConfiguration.HttpClientCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
import jakarta.inject.Singleton;
@@ -17,39 +16,53 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Lombok;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Message;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.http.nio.AsyncEntityConsumer;
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Smart HTTP getter: handles gzip, ssl, last modified and etag headers */
/** Smart HTTP getter: handles gzip, ssl, last modified, and etag headers */
@Singleton
@Slf4j
public class HttpGetter {
private final CommaFeedConfiguration config;
private final InstantSource instantSource;
private final CloseableHttpClient client;
private final CloseableHttpAsyncClient client;
private final Cache<HttpRequest, HttpResponse> cache;
public HttpGetter(
@@ -150,70 +163,96 @@ public class HttpGetter {
HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
// causes issues with some feeds
// see https://github.com/Athou/commafeed/issues/1572
// and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
.setProtocolUpgradeEnabled(false)
.build());
return client.execute(
request.toClassicHttpRequest(),
context,
resp -> {
byte[] content =
resp.getEntity() == null
? null
: toByteArray(
resp.getEntity(),
config.httpClient().maxResponseSize().asLongValue());
int code = resp.getCode();
String lastModifiedHeader =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.orElse(null);
String eTagHeader =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.orElse(null);
long maxResponseSize = config.httpClient().maxResponseSize().asLongValue();
AsyncRequestProducer req = SimpleRequestProducer.create(request.toSimpleHttpRequest());
AsyncResponseConsumer<Message<org.apache.hc.core5.http.HttpResponse, byte[]>> resp =
new BasicResponseConsumer<>(
new LimitedContentLengthEntityConsumer(
new LimitedBytesEntityConsumer(maxResponseSize), maxResponseSize));
FutureCallback<Message<org.apache.hc.core5.http.HttpResponse, byte[]>> callback =
new FutureCallback<>() {
@Override
public void completed(
Message<org.apache.hc.core5.http.HttpResponse, byte[]> result) {}
CacheControl cacheControl =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.map(HttpGetter::toCacheControl)
.orElse(null);
@Override
public void failed(Exception ex) {}
Instant retryAfter =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.map(this::toInstant)
.orElse(null);
@Override
public void cancelled() {}
};
Future<Message<org.apache.hc.core5.http.HttpResponse, byte[]>> future =
client.execute(req, resp, context, callback);
String contentType =
Optional.ofNullable(resp.getEntity())
.map(HttpEntity::getContentType)
.orElse(null);
String urlAfterRedirect =
Optional.ofNullable(context.getRedirectLocations())
.map(RedirectLocations::getAll)
.map(l -> Iterables.getLast(l, null))
.map(URI::toString)
.orElse(request.getUrl());
try {
Message<org.apache.hc.core5.http.HttpResponse, byte[]> message =
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS);
return new HttpResponse(
code,
lastModifiedHeader,
eTagHeader,
cacheControl,
retryAfter,
content,
contentType,
urlAfterRedirect);
});
byte[] content = message.getBody();
int code = message.getHead().getCode();
String lastModifiedHeader =
Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.LAST_MODIFIED))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.orElse(null);
String eTagHeader =
Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.ETAG))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.orElse(null);
CacheControl cacheControl =
Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.CACHE_CONTROL))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.map(HttpGetter::toCacheControl)
.orElse(null);
Instant retryAfter =
Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.RETRY_AFTER))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.map(this::toInstant)
.orElse(null);
String contentType =
Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.CONTENT_TYPE))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.orElse(null);
String urlAfterRedirect =
Optional.ofNullable(context.getRedirectLocations())
.map(RedirectLocations::getAll)
.map(l -> Iterables.getLast(l, null))
.map(URI::toString)
.orElse(request.getUrl());
return new HttpResponse(
code,
lastModifiedHeader,
eTagHeader,
cacheControl,
retryAfter,
content,
contentType,
urlAfterRedirect);
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
throw new RuntimeException("interrupted while fetching " + request.getUrl(), e);
} catch (TimeoutException e) {
future.cancel(true);
throw new IOException("timed out while fetching " + request.getUrl(), e);
} catch (ExecutionException e) {
future.cancel(true);
throw Lombok.sneakyThrow(e.getCause());
}
}
private static CacheControl toCacheControl(String headerValue) {
@@ -237,28 +276,6 @@ public class HttpGetter {
return DateUtils.parseStandardDate(headerValue);
}
private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
if (entity.getContentLength() > maxBytes) {
throw new IOException(
"Response size (%s bytes) exceeds the maximum allowed size (%s bytes)"
.formatted(entity.getContentLength(), maxBytes));
}
try (InputStream input = entity.getContent()) {
if (input == null) {
return null;
}
byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes();
if (bytes.length > maxBytes) {
throw new IOException(
"Response size exceeds the maximum allowed size (%s bytes)"
.formatted(maxBytes));
}
return bytes;
}
}
private static Cache<HttpRequest, HttpResponse> newCache(CommaFeedConfiguration config) {
HttpClientCache cacheConfig = config.httpClient().cache();
if (!cacheConfig.enabled()) {
@@ -274,6 +291,60 @@ public class HttpGetter {
.build();
}
private record LimitedContentLengthEntityConsumer(
@Delegate AsyncEntityConsumer<byte[]> delegate, long maxBytes)
implements AsyncEntityConsumer<byte[]> {
@Override
public void streamStart(EntityDetails entityDetails, FutureCallback<byte[]> resultCallback)
throws HttpException, IOException {
long contentLength = entityDetails.getContentLength();
if (contentLength > maxBytes) {
throw new IOException(
"Response size (%s bytes) exceeds the maximum allowed size (%s bytes)"
.formatted(contentLength, maxBytes));
}
delegate.streamStart(entityDetails, resultCallback);
}
}
@RequiredArgsConstructor
private static class LimitedBytesEntityConsumer extends AbstractBinAsyncEntityConsumer<byte[]> {
private final long maxBytes;
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
@Override
protected void streamStart(ContentType contentType) {}
@Override
protected int capacityIncrement() {
return Integer.MAX_VALUE;
}
@Override
protected void data(ByteBuffer src, boolean endOfStream) throws IOException {
if (out.size() + src.remaining() > maxBytes) {
throw new IOException(
"Response size exceeds the maximum allowed size (%s bytes)"
.formatted(maxBytes));
}
byte[] chunk = new byte[src.remaining()];
src.get(chunk);
out.write(chunk, 0, chunk.length);
}
@Override
protected byte[] generateContent() {
return out.toByteArray();
}
@Override
public void releaseResources() {
out.reset();
}
}
public static class SchemeNotAllowedException extends Exception {
private static final long serialVersionUID = 1L;
@@ -336,15 +407,15 @@ public class HttpGetter {
return new HttpRequestBuilder().url(url);
}
public ClassicHttpRequest toClassicHttpRequest() {
ClassicHttpRequest req = ClassicRequestBuilder.get(url).build();
public SimpleHttpRequest toSimpleHttpRequest() {
SimpleRequestBuilder builder = SimpleRequestBuilder.get(url);
if (lastModified != null) {
req.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
builder.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
}
if (eTag != null) {
req.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
builder.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
}
return req;
return builder.build();
}
}

View File

@@ -23,7 +23,6 @@ import jakarta.inject.Singleton;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jdom2.Element;
import org.jdom2.Namespace;
@@ -58,12 +57,6 @@ public class FeedParser {
public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
this.encodingDetector = encodingDetector;
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 {

View File

@@ -148,6 +148,7 @@ public class FeedEntryContentCleaningService {
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("iframe", "src", "http", "https");
whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https");

View File

@@ -98,15 +98,14 @@ public class FeedEntryService {
}
}
public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
if (sub == null) {
public void starEntry(User user, Long entryId, boolean starred) {
FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) {
return;
}
FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) {
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
if (sub == null) {
return;
}

View File

@@ -2,8 +2,10 @@ package com.commafeed.backend.service;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import jakarta.inject.Singleton;
@@ -20,6 +22,7 @@ public class FeedEntryTagService {
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryTagDAO feedEntryTagDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
public void updateTags(User user, Long entryId, List<String> tagNames) {
FeedEntry entry = feedEntryDAO.findById(entryId);
@@ -27,6 +30,11 @@ public class FeedEntryTagService {
return;
}
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
if (sub == null) {
return;
}
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
Set<String> existingTagNames =
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());

View File

@@ -16,26 +16,33 @@ import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.Message;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
import org.apache.hc.core5.net.WWWFormCodec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Singleton
@Slf4j
public class PushNotificationService {
private final CloseableHttpClient httpClient;
private final CloseableHttpAsyncClient httpClient;
private final Meter meter;
private final CommaFeedConfiguration config;
@@ -97,13 +104,9 @@ public class PushNotificationService {
return;
}
HttpPost request = new HttpPost(serverUrl + "/" + topic);
request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/" + topic);
request.addHeader("Title", feedTitle);
request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8));
request.setBody(entryTitle, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
if (StringUtils.isNotBlank(entryUrl)) {
request.addHeader("Click", entryUrl);
@@ -113,15 +116,7 @@ public class PushNotificationService {
request.addHeader("Authorization", "Bearer " + settings.getUserSecret());
}
httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) {
throw new PushNotificationException(
"ntfy notification failed with status " + response.getCode());
}
return null;
});
send("ntfy", request.build());
}
private void sendGotify(
@@ -152,23 +147,11 @@ public class PushNotificationService {
.put("click", new JsonObject().put("url", entryUrl))));
}
HttpPost request = new HttpPost(serverUrl + "/message");
request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/message");
request.addHeader("X-Gotify-Key", token);
request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON));
request.setBody(json.toString(), ContentType.APPLICATION_JSON);
httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) {
throw new PushNotificationException(
"gotify notification failed with status " + response.getCode());
}
return null;
});
send("gotify", request.build());
}
private void sendPushover(
@@ -194,22 +177,40 @@ public class PushNotificationService {
params.add(new BasicNameValuePair("url", entryUrl));
}
HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json");
request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
SimpleRequestBuilder request =
SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json");
request.setBody(
WWWFormCodec.format(params, StandardCharsets.UTF_8),
ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8));
httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) {
throw new PushNotificationException(
"pushover notification failed with status " + response.getCode());
}
return null;
});
send("pushover", request.build());
}
private void send(String name, SimpleHttpRequest request) throws IOException {
Future<Message<HttpResponse, Object>> future =
httpClient.execute(
SimpleRequestProducer.create(request),
new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()),
null);
try {
HttpResponse response =
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS)
.getHead();
if (response.getCode() >= 400) {
throw new PushNotificationException(
name + " notification failed with status " + response.getCode());
}
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
throw new IOException("interrupted while sending " + name + " notification", e);
} catch (TimeoutException e) {
future.cancel(true);
throw new IOException("timed out while sending " + name + " notification", e);
} catch (ExecutionException e) {
future.cancel(true);
throw new IOException(e.getCause());
}
}
public static class PushNotificationException extends RuntimeException {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -19,9 +19,6 @@ public class StarRequest implements Serializable {
@Size(max = 128)
private String id;
@Schema(description = "feed id", required = true)
private Long feedId;
@Schema(description = "starred or not", required = true)
private boolean starred;
}

View File

@@ -89,11 +89,9 @@ public class EntryREST {
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getFeedId());
User user = authenticationContext.getCurrentUser();
feedEntryService.starEntry(
user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.isStarred());
return Response.ok().build();
}

View File

@@ -413,7 +413,8 @@ public class UserREST {
}
private String buildEmailContent(User user) throws URISyntaxException {
String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString());
String publicUrl =
Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow());
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.",
user.getName(), callbackUrl(user, publicUrl));

View File

@@ -388,9 +388,7 @@ public class FeverREST {
if ("read".equals(action) || "unread".equals(action)) {
feedEntryService.markEntry(user, id, "read".equals(action));
} else if ("saved".equals(action) || "unsaved".equals(action)) {
FeedEntry entry = feedEntryDAO.findById(id);
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
feedEntryService.starEntry(user, id, "saved".equals(action));
}
} else if ("feed".equals(source)) {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);

View File

@@ -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;
}
}

View File

@@ -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) {}
}

View File

@@ -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/&lt;id&gt;" 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;
}
}
}

View File

@@ -4,8 +4,9 @@ import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
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.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
@@ -48,7 +49,8 @@ public class DatabaseUsernamePasswordIdentityProvider
new String(
request.getPassword().getPassword())));
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()));

View File

@@ -16,7 +16,7 @@ import java.util.Optional;
import java.util.Set;
@Singleton
public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism {
public class ApiKeyAuthenticationMechanism implements HttpAuthenticationMechanism {
@Override
public Uni<SecurityIdentity> authenticate(

View File

@@ -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=&lt;token&gt;" 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);
}
}

View File

@@ -3,7 +3,6 @@ quarkus.http.port=8082
quarkus.http.test-port=8085
quarkus.http.enable-compression=true
quarkus.http.enable-decompression=true
# http cache
quarkus.http.static-resources.max-age=P365d
## 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
quarkus.http.filter.next.header."Cache-Control"=no-cache
quarkus.http.filter.next.matches=/next
# security
quarkus.http.auth.basic=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.login-page=
quarkus.http.auth.form.error-page=
# websocket
quarkus.websocket.dispatch-to-worker=true
# database
quarkus.liquibase.change-log=migrations.xml
quarkus.liquibase.migrate-at-start=true
# openapi
quarkus.smallrye-openapi.path=/openapi
quarkus.swagger-ui.path=/api-documentation
quarkus.swagger-ui.always-include=true
# shutdown
quarkus.shutdown.timeout=5s
# native
quarkus.native.march=compatibility
quarkus.native.add-all-charsets=true
# fix for https://github.com/Athou/commafeed/issues/1795
quarkus.native.additional-build-args=-H:PageSize=65536
# dev profile overrides
%dev.quarkus.http.port=8083
%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890
%dev.quarkus.log.category."com.commafeed".level=DEBUG
# %dev.quarkus.hibernate-orm.log.sql=true
%dev.commafeed.users.create-demo-account=true
# test profile overrides
%test.quarkus.log.category."org.mockserver".level=WARN
%test.quarkus.log.category."liquibase".level=WARN
%test.quarkus.log.level=WARN
%test.quarkus.log.category."org.mockserver".level=ERROR
%test.commafeed.shutdown-timeout=100ms
%test.commafeed.users.create-demo-account=true
%test.commafeed.users.allow-registrations=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.block-local-addresses=false
%test.commafeed.database.cleanup.entries-max-age=0
%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
%test.commafeed.feed-refresh.filtering-expression-evaluation-timeout=5s
# prod profile overrides
%prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE
%prod.quarkus.datasource.username=sa

View File

@@ -11,6 +11,8 @@ import com.google.common.net.HttpHeaders;
import io.quarkus.runtime.configuration.MemorySize;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.core5.http.HttpStatus;
@@ -29,12 +31,12 @@ import org.mockserver.model.Delay;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.mockserver.model.MediaType;
import org.mockserver.model.Protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
@@ -45,6 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
@Slf4j
class HttpGetterTest {
private static final Instant NOW = Instant.now();
@@ -237,7 +240,7 @@ class HttpGetterTest {
.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl));
Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
}
@Test
@@ -416,7 +419,7 @@ class HttpGetterTest {
@Nested
class Compression {
private static final String ACCEPT_ENCODING = "gzip, deflate, br";
private static final String ACCEPT_ENCODING = "deflate, gzip";
@Test
void gzip() throws Exception {
@@ -437,24 +440,30 @@ class HttpGetterTest {
.when(HttpRequest.request().withMethod("GET"))
.respond(
req -> {
String acceptEncodingHeader =
req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
throw new Exception(
"Wrong value in the Accept-Encoding header, should be '%s' but was '%s'"
.formatted(
ACCEPT_ENCODING, acceptEncodingHeader));
}
try {
String acceptEncodingHeader =
req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
throw new Exception(
"Wrong value in the Accept-Encoding header, should be '%s' but was '%s'"
.formatted(
ACCEPT_ENCODING,
acceptEncodingHeader));
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (OutputStream compressionOutputStream =
compressionFunction.apply(output)) {
compressionOutputStream.write(body.getBytes());
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (OutputStream compressionOutputStream =
compressionFunction.apply(output)) {
compressionOutputStream.write(body.getBytes());
}
return HttpResponse.response()
.withBody(output.toByteArray())
.withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
return HttpResponse.response()
.withBody(output.toByteArray())
.withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
} catch (Exception e) {
log.error("Error in compression callback", e);
throw e;
}
});
HttpResult result = getter.get(HttpGetterTest.this.feedUrl);
@@ -547,5 +556,73 @@ class HttpGetterTest {
Assertions.assertThrows(
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
}
}
@Nested
class Protocols {
@Test
void usesHttp2OverTls() throws Exception {
mockServerClient
.when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_2))
.respond(HttpResponse.response().withBody("h2"));
mockServerClient
.when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_1_1))
.respond(HttpResponse.response().withBody("h1"));
HttpResult result = getter.get("https://localhost:" + mockServerClient.getPort());
Assertions.assertEquals("h2", new String(result.content()));
}
@Test
void fallsBackToHttp1OverCleartext() throws Exception {
mockServerClient
.when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_1_1))
.respond(HttpResponse.response().withBody("h1"));
HttpResult result = getter.get("http://localhost:" + mockServerClient.getPort());
Assertions.assertEquals("h1", new String(result.content()));
}
}
}

View File

@@ -15,7 +15,7 @@ class FeedEntryContentCleaningServiceTest {
<p>
Some text
<img width="965" height="320" src="https://localhost/an-image.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="alt-desc" decoding="async" sizes="(max-width: 965px) 100vw, 965px" style="width: 100%; opacity: 0">
<iframe src="url" style="width: 100%; opacity: 0"></iframe>
<iframe src="https://localhost/an-iframe" style="width: 100%; opacity: 0"></iframe>
<forbidden-element>aaa</forbidden-element>
""";
String result = feedEntryContentCleaningService.clean(content, "baseUri", false);
@@ -25,7 +25,7 @@ class FeedEntryContentCleaningServiceTest {
<p>
Some text
<img width="965" height="320" src="https://localhost/an-image.png" alt="alt-desc" style="width:100%;">
<iframe src="url" style="width:100%;"></iframe>
<iframe src="https://localhost/an-iframe" style="width:100%;"></iframe>
aaa
</p>
"""

View File

@@ -36,7 +36,7 @@ class AuthentificationIT {
page.navigate(getLoginPageUrl());
PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password");
PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT))
.containsText("wrong username or password");
.containsText("Wrong username or password");
}
@Test

View File

@@ -1,7 +1,9 @@
package com.commafeed.integration;
import com.commafeed.ExceptionMappers.CommaFeedApplicationError;
import com.commafeed.ExceptionMappers.UnauthorizedResponse;
import com.commafeed.TestConstants;
import com.commafeed.frontend.exception.CommaFeedExceptionType;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.MarkRequest;
@@ -57,6 +59,27 @@ class SecurityIT extends BaseIT {
.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
void basicAuthLogin() {
RestAssured.given()

View File

@@ -45,7 +45,6 @@ class DatabaseCleaningIT extends BaseIT {
private void starEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest();
starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true);
RestAssured.given()
.body(starRequest)
@@ -58,7 +57,6 @@ class DatabaseCleaningIT extends BaseIT {
private void unstarEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest();
starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(false);
RestAssured.given()
.body(starRequest)

View File

@@ -240,7 +240,6 @@ class CategoryIT extends BaseIT {
StarRequest starRequest = new StarRequest();
starRequest.setId(entry.getId());
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true);
RestAssured.given()
.body(starRequest)

View File

@@ -121,7 +121,6 @@ class FeverIT extends BaseIT {
StarRequest starRequest = new StarRequest();
starRequest.setId(entry.getId());
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true);
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star");

View File

@@ -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);
}
}
}

View File

@@ -52,6 +52,7 @@ class UserIT extends BaseIT {
RestAssured.given()
.body(req)
.contentType(ContentType.JSON)
.header("Host", "malicious.url.com")
.post("rest/user/passwordReset")
.then()
.statusCode(200);
@@ -64,6 +65,7 @@ class UserIT extends BaseIT {
Assertions.assertTrue(
message.getHtml()
.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());
Element a = Jsoup.parse(message.getHtml()).select("a").getFirst();

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>7.2.1</version>
<version>7.4.1</version>
<name>CommaFeed</name>
<packaging>pom</packaging>
@@ -19,7 +19,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<version>3.16.0</version>
<configuration>
<parameters>true</parameters>