Compare commits

..

197 Commits
4.5.0 ... 5.0.2

Author SHA1 Message Date
Athou
7e74d2f6f4 release 5.0.2 2024-08-20 07:27:46 +02:00
Athou
dc25d53dc0 github actions is sometimes slow, increase timeout for tests 2024-08-20 00:03:45 +02:00
Athou
ac1a927836 remove google-api-services-youtube because it doesn't play nicely with native-image 2024-08-19 23:55:38 +02:00
Athou
b50b69adb2 Revert "better workaround for cookie max-age" because it causes issues with favicon fetching 2024-08-19 18:27:50 +02:00
Athou
b112e912af release 5.0.1 2024-08-19 15:55:13 +02:00
Athou
ece55727d3 better workaround for cookie max-age 2024-08-19 14:31:12 +02:00
Athou
181dd24b57 format both Dockerfiles the same way 2024-08-19 10:29:35 +02:00
Athou
10008ca0e8 add link to all quarkus settings 2024-08-19 09:38:26 +02:00
Athou
134c4621a8 docker README tweaks 2024-08-19 08:47:23 +02:00
Athou
51f15bf487 github actions is sometimes very slow, increase default timeouts for tests 2024-08-19 08:26:06 +02:00
renovate[bot]
49ae2c88ad Lock file maintenance 2024-08-19 05:53:45 +00:00
Jérémie Panzer
43a628fc55 Merge pull request #1523 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0
2024-08-19 07:51:49 +02:00
Jérémie Panzer
7f71f95f7c Merge pull request #1522 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0
2024-08-19 07:51:41 +02:00
Athou
e8d5eab419 compile to native image with support for older CPUs 2024-08-19 07:08:18 +02:00
renovate[bot]
de3a6b1f20 Update dependency io.dropwizard.metrics:metrics-json to v4.2.27 2024-08-19 00:25:33 +00:00
renovate[bot]
849742e19a Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0 2024-08-18 21:04:13 +00:00
renovate[bot]
b6392b114c Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0 2024-08-18 21:04:10 +00:00
Athou
4db0c775ff add settings documentation 2024-08-18 21:23:42 +02:00
Athou
ff9374f1ed README tweak 2024-08-18 19:20:34 +02:00
Athou
ea86c9bb1f README tweak 2024-08-18 17:07:31 +02:00
Athou
e6dd088abe fix typo 2024-08-18 17:00:59 +02:00
Jérémie Panzer
c039d8f3a4 Merge pull request #1521 from Athou/renovate/com.google.apis-google-api-services-youtube-3.0.x
Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0
2024-08-18 14:51:39 +02:00
renovate[bot]
bffa6329fd Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0 2024-08-18 10:58:38 +00:00
Athou
b88e5d2847 increase ssl handshake timeout during tests to fix build on slower machines 2024-08-18 12:25:52 +02:00
Jérémie Panzer
0fc4fcd406 Merge pull request #1516 from Athou/renovate/react-icons-5.x
Update dependency react-icons to ^5.3.0
2024-08-18 10:39:10 +02:00
Athou
f04ca21394 Twitter has been renamed X 2024-08-18 10:15:17 +02:00
renovate[bot]
a82fca130f Update dependency react-icons to ^5.3.0 2024-08-18 07:58:41 +00:00
Athou
70d494798c Docker readme tweaks 2024-08-18 09:45:44 +02:00
Athou
cf02bf221b release 5.0.0 2024-08-18 09:35:36 +02:00
renovate[bot]
9eb03d7455 Update quarkus.version to v3.13.2 2024-08-18 08:21:57 +02:00
renovate[bot]
12c8fdeec2 Update ibm-semeru-runtimes Docker tag to open-21.0.4_7-jre 2024-08-18 05:58:53 +00:00
Athou
851babfe2a remove quarkus branch from ci 2024-08-18 07:55:51 +02:00
Jérémie Panzer
859490806b Merge pull request #1520 from Athou/quarkus
Migrate to Quarkus (#1517)
2024-08-18 07:54:43 +02:00
renovate[bot]
2c828b50da Update dependency @fontsource/open-sans to ^5.0.29 2024-08-18 01:32:31 +00:00
Athou
ede7834cb8 configurable filtering expression evaluation timeout 2024-08-17 23:26:42 +02:00
Athou
3627ee369d version dockerhub readme and update it automatically on release 2024-08-17 23:16:19 +02:00
Athou
c4c41d1494 increase timeout a little bit because github actions are laggy 2024-08-17 22:29:40 +02:00
Athou
c577e77f8f README tweaks 2024-08-17 22:27:30 +02:00
Athou
9218f19832 javadoc tweaks 2024-08-17 22:11:47 +02:00
renovate[bot]
ecbc2133a4 Update dependency maven to v3.9.9 2024-08-17 20:10:04 +00:00
Athou
e38ca66c51 try to fix "Illegal attempt to associate a ManagedEntity with two open persistence contexts" 2024-08-17 16:22:46 +02:00
Athou
2395a2670e add ResourceBundle needed for cssparser in native image 2024-08-17 00:43:32 +02:00
Athou
e7748d787f no need to start a transaction to fetch favicons 2024-08-16 22:29:12 +02:00
Athou
012ce71134 configurable http client timeouts 2024-08-16 21:16:02 +02:00
Athou
1b1a3f49c1 keep using the same css parser as before 2024-08-16 19:01:44 +02:00
Athou
5b77860189 use join to speed up cleanup 2024-08-16 17:42:15 +02:00
Athou
b333e8d90a set a timeout on ssl handshakes 2024-08-16 14:40:16 +02:00
Athou
ab6457ef3f README tweaks 2024-08-16 14:23:18 +02:00
Athou
5c69daec08 restore welcome page on 401 2024-08-16 14:02:49 +02:00
Athou
1bfa3ebb8e set the default h2 path to a relative one next to the executable 2024-08-16 13:47:15 +02:00
Athou
2694fea211 cleanup 2024-08-16 08:45:30 +02:00
Athou
720eddeb66 CHANGELOG tweaks 2024-08-16 08:31:23 +02:00
Athou
ab334a7bc6 feed needs to be known to be deleted 2024-08-16 07:31:22 +02:00
Athou
214dfe580a more rome classes 2024-08-16 07:22:25 +02:00
Athou
4ef53eab3a mute rome modules warnings in production 2024-08-16 06:19:35 +02:00
Athou
2f51547f0d add missing rome classes 2024-08-16 06:15:52 +02:00
Athou
da910ac336 mute MediaModuleParser warnings 2024-08-16 05:53:23 +02:00
Athou
643954f7c9 timeout should be an Integer 2024-08-16 05:48:01 +02:00
renovate[bot]
63061482d0 Update dependency vite to ^5.4.1 2024-08-15 19:16:05 +00:00
renovate[bot]
86d4f5a670 Update dependency react-router-dom to ^6.26.1 2024-08-15 17:31:29 +00:00
Athou
815093f1c6 remove STARTUP_TIME because static fields are initialized at compile time in native mode 2024-08-15 12:17:28 +02:00
Athou
47d39831d3 use Duration for query timeout 2024-08-15 09:05:56 +02:00
Athou
c18ed829aa generate jvm package with a -jvm suffix 2024-08-14 23:08:24 +02:00
Athou
e757e61b79 config comment tweaks 2024-08-14 21:34:05 +02:00
Athou
d612d83874 resolve public url dynamically, remove publicUrl config element 2024-08-14 21:07:45 +02:00
Athou
e170dfe60b prepare 5.0.0 changelog 2024-08-14 20:43:54 +02:00
Athou
69cd90edd8 only use rest-assured for tests 2024-08-14 16:00:47 +02:00
renovate[bot]
f506f722c2 Update dependency axios to ^1.7.4 2024-08-13 20:16:28 +00:00
Athou
857736adad README tweaks 2024-08-13 17:26:51 +02:00
Athou
a92df774bd rome needs to clone Date in native mode 2024-08-13 16:13:05 +02:00
Athou
f2c6734c79 fix warning in native mode about parser not found 2024-08-13 16:12:40 +02:00
Athou
77b6cf75a5 README tweaks 2024-08-13 15:05:40 +02:00
Athou
3b56496196 javadoc tweaks 2024-08-13 12:49:04 +02:00
Athou
aabbf0a5d1 use a relative link 2024-08-13 12:49:04 +02:00
Athou
9a43fd434f ci for quarkus branch 2024-08-13 12:49:04 +02:00
Athou
21ce9db4b0 README update 2024-08-13 12:49:04 +02:00
Athou
044694487d remove redis as caching is no longer needed now 2024-08-13 12:49:04 +02:00
Athou
3af8485326 TODO remove redis 2024-08-13 12:49:04 +02:00
Athou
f7adef0648 add windows builds 2024-08-13 12:49:04 +02:00
Athou
dc16e43154 add release ci job 2024-08-13 12:49:04 +02:00
Athou
78a5267198 fix opml import encoding issue 2024-08-13 12:49:04 +02:00
Athou
04af355e0c remove unused timers page 2024-08-13 12:49:04 +02:00
Athou
89405009ec set a Max-Age on the auth cookie 2024-08-13 12:49:04 +02:00
Athou
6b0aa32da2 use profile instead of system property to set db-kind 2024-08-13 12:49:04 +02:00
Athou
aaf237d111 use quarkus mailer for password recovery 2024-08-13 12:49:04 +02:00
Athou
1fd48a0a40 merge docker amd64 and arm64 tags 2024-08-13 12:49:03 +02:00
Athou
09e0a51b46 restore Docker workflow 2024-08-13 12:49:00 +02:00
Athou
cc32f8ad16 WIP 2024-08-13 12:48:37 +02:00
renovate[bot]
2f6ddf0e70 Update mariadb Docker tag to v11.4.3 2024-08-13 00:48:48 +00:00
renovate[bot]
c3973755da Update ibm-semeru-runtimes Docker tag to open-21.0.4_7-jre 2024-08-12 23:34:26 +00:00
renovate[bot]
42537a65b9 Update dependency com.h2database:h2 to v2.3.232 2024-08-12 19:41:12 +00:00
Athou
906c92e54f fix badge layout 2024-08-12 21:13:46 +02:00
renovate[bot]
cc69968d78 Update mantine monorepo to ^7.12.1 2024-08-12 14:48:18 +00:00
renovate[bot]
dcde2083ec Lock file maintenance 2024-08-12 01:26:56 +00:00
Jérémie Panzer
7469784059 Merge pull request #1514 from Athou/renovate/com.microsoft.playwright-playwright-1.x
Update dependency com.microsoft.playwright:playwright to v1.46.0
2024-08-10 12:14:13 +02:00
renovate[bot]
c13a693456 Update dependency com.microsoft.playwright:playwright to v1.46.0 2024-08-09 23:05:37 +00:00
Jérémie Panzer
e3c482d664 Merge pull request #1510 from Athou/renovate/vite-5.x
Update dependency vite to ^5.4.0
2024-08-09 16:48:43 +02:00
renovate[bot]
1fd33a5585 Update dependency vite to ^5.4.0 2024-08-09 14:34:49 +00:00
Jérémie Panzer
0742778e6a Merge pull request #1513 from Athou/renovate/patch-linguijs-monorepo
Update linguijs monorepo to ^4.11.3 (patch)
2024-08-09 16:33:03 +02:00
Jérémie Panzer
152479c888 Merge pull request #1511 from Athou/renovate/vite-tsconfig-paths-5.x
Update dependency vite-tsconfig-paths to v5
2024-08-09 16:32:23 +02:00
Jérémie Panzer
a297f8c0c8 Merge pull request #1512 from Athou/renovate/postgres-16.x
Update postgres Docker tag to v16.4
2024-08-09 16:31:59 +02:00
renovate[bot]
92aeee0572 Update linguijs monorepo to ^4.11.3 2024-08-09 11:41:52 +00:00
renovate[bot]
050756517e Update dependency vite-tsconfig-paths to v5 2024-08-08 22:34:31 +00:00
renovate[bot]
0bb46f291a Update postgres Docker tag to v16.4 2024-08-08 22:34:10 +00:00
Jérémie Panzer
1eecabf105 Merge pull request #1508 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.12.0 (minor)
2024-08-05 19:16:47 +02:00
renovate[bot]
da1bd8d32e Update mantine monorepo to ^7.12.0 2024-08-05 16:28:35 +00:00
Athou
124983a396 rename metrics a little 2024-08-05 09:05:01 +02:00
Athou
43613688da evict unused HTTP connections 2024-08-05 08:41:12 +02:00
Athou
43aa69cd18 don't crash vite on error in dev mode 2024-08-05 08:28:17 +02:00
Athou
780b7666c5 add metrics for HttpGetter connection pool 2024-08-05 08:28:17 +02:00
renovate[bot]
70b4534e14 Lock file maintenance 2024-08-05 01:13:51 +00:00
Athou
24666fd7fc migrate to lingui.config.ts to benefit from typing 2024-08-03 22:29:09 +02:00
Athou
de80aa6bb3 replace t` with msg` to fix labels not being translated correctly 2024-08-03 13:09:15 +02:00
Athou
6c7e2ea847 add missing translation key for the Cmd key on MacOS 2024-08-03 12:56:56 +02:00
Athou
6ea318acd3 remove right section as we don't show keyboard shortcuts anywhere else 2024-08-03 12:54:57 +02:00
Athou
2f4ee7cff8 add data attributes to tree elements (#1507) 2024-08-03 12:37:33 +02:00
Athou
9d9d758fa6 use article instead of div (#1507) 2024-08-03 11:34:36 +02:00
Athou
a071b7c265 add aria-label to action buttons (#1507) 2024-08-03 11:30:29 +02:00
Athou
3a57b68fa3 use a different icon for filtering unread entries and marking an entry as read (#1506) 2024-08-03 10:24:06 +02:00
Jérémie Panzer
f2f36baf1b Merge pull request #1505 from Athou/renovate/querydsl.version
Update querydsl.version to v6.6 (minor)
2024-08-02 19:28:04 +02:00
renovate[bot]
1aaf9e747a Update querydsl.version to v6.6 2024-08-02 17:00:53 +00:00
Athou
92611772a9 reduce bottom margin slightly to avoid having a scrollbar in the extension popup when there are no entries 2024-08-02 10:37:40 +02:00
renovate[bot]
fb159dc46b Update dependency axios to ^1.7.3 2024-08-01 16:19:28 +00:00
Jérémie Panzer
78ea0873f2 Merge pull request #1504 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.26.0
2024-08-01 18:18:45 +02:00
renovate[bot]
faabc01dbc Update dependency react-router-dom to ^6.26.0 2024-08-01 13:48:59 +00:00
renovate[bot]
5acfe9e92a Update dependency tss-react to ^4.9.12 2024-08-01 12:45:12 +00:00
renovate[bot]
4388a8b6ce Update testcontainers-java monorepo to v1.20.1 2024-07-31 16:31:47 +00:00
renovate[bot]
7414bd15b0 Update dependency vitest to ^2.0.5 2024-07-31 13:41:55 +00:00
Jérémie Panzer
52d6021f3c Merge pull request #1503 from Athou/renovate/redis-7.x
Update redis Docker tag to v7.4.0
2024-07-30 07:14:48 +02:00
renovate[bot]
f7acc27fcb Update redis Docker tag to v7.4.0 2024-07-30 04:46:37 +00:00
renovate[bot]
175a293327 Update dependency redis.clients:jedis to v5.1.4 2024-07-29 17:17:09 +00:00
renovate[bot]
21dd6519b0 Update bouncycastle.version to v1.78.1 2024-07-29 11:25:29 +00:00
Athou
72f55c34b7 add test to make sure HttpGetter supports compression 2024-07-29 13:20:18 +02:00
Athou
1b1d3c791b add test to make sure HttpGetter ignores invalid certificates 2024-07-29 10:39:35 +02:00
renovate[bot]
159c2c01a7 Lock file maintenance 2024-07-29 00:18:18 +00:00
Athou
272f5b42f9 simplify stackoverflow urls 2024-07-28 09:58:24 +02:00
renovate[bot]
2395d0782e Update dependency @reduxjs/toolkit to ^2.2.7 2024-07-27 18:19:48 +00:00
Athou
da81830e43 add a test case for feeds that do not return a content length 2024-07-27 01:01:11 +02:00
renovate[bot]
63a602cf8a Update dependency tss-react to ^4.9.11 2024-07-26 16:23:31 +00:00
renovate[bot]
0244b5c3e3 Update dependency vite to ^5.3.5 2024-07-25 12:40:59 +00:00
Jérémie Panzer
9592e86fa9 Merge pull request #1497 from Athou/renovate/node-20.x
Update dependency node to v20.16.0
2024-07-24 21:11:44 +02:00
renovate[bot]
e6840bb50c Update dependency node to v20.16.0 2024-07-24 16:31:02 +00:00
Jérémie Panzer
b6890378a1 Merge pull request #1496 from Athou/renovate/vitest-mock-extended-2.x
Update dependency vitest-mock-extended to v2
2024-07-23 21:56:08 +02:00
renovate[bot]
ba72ed0b93 Update dependency vitest-mock-extended to v2 2024-07-23 19:45:08 +00:00
renovate[bot]
e2fb576858 Update ibm-semeru-runtimes Docker tag to open-21.0.3_9-jre 2024-07-23 15:40:19 +00:00
Athou
608b099b4d make renovate pickup semeru 2024-07-23 17:39:37 +02:00
renovate[bot]
c2e0c81f7e Update mysql Docker tag to v9.0.1 2024-07-23 07:16:23 +00:00
renovate[bot]
7071d01a59 Update dependency typescript to ^5.5.4 2024-07-23 04:26:11 +00:00
renovate[bot]
30cd2b9b53 Update dependency com.microsoft.playwright:playwright to v1.45.1 2024-07-23 00:05:41 +00:00
Athou
abc498b09c semeru already defines a JAVA_TOOL_OPTIONS variable with a shared classes cache, we don't want to override it 2024-07-22 16:52:30 +02:00
renovate[bot]
31081e1089 Update dependency vitest to ^2.0.4 2024-07-22 09:53:53 +00:00
renovate[bot]
4a16b8d072 Lock file maintenance 2024-07-22 02:12:54 +00:00
Jérémie Panzer
9c04095292 Merge pull request #1492 from Athou/renovate/emotion-monorepo
Update dependency @emotion/react to ^11.13.0
2024-07-20 14:55:31 +02:00
renovate[bot]
643f98d59e Update dependency @emotion/react to ^11.13.0 2024-07-20 09:06:33 +00:00
Jérémie Panzer
f4da19183e Merge pull request #1491 from Athou/renovate/emotion-monorepo
Update dependency @emotion/react to ^11.12.0
2024-07-19 09:56:36 +02:00
renovate[bot]
de40f253b5 Update dependency @emotion/react to ^11.12.0 2024-07-19 07:28:55 +00:00
renovate[bot]
f6543e407a Update dependency dayjs to ^1.11.12 2024-07-18 13:26:56 +00:00
renovate[bot]
4d462a8e9e Update dependency react-router-dom to ^6.25.1 2024-07-17 22:23:50 +00:00
Jérémie Panzer
018ee1f3e6 Merge pull request #1490 from Athou/renovate/testcontainers-java-monorepo
Update testcontainers-java monorepo to v1.20.0 (minor)
2024-07-18 00:22:46 +02:00
renovate[bot]
752268fed1 Update testcontainers-java monorepo to v1.20.0 2024-07-17 22:10:29 +00:00
renovate[bot]
8fe9a6cc3a Update dependency org.mariadb.jdbc:mariadb-java-client to v3.4.1 2024-07-17 20:48:37 +00:00
Athou
b17a17ba10 don't parse feeds that are too large to prevent memory issues 2024-07-16 21:20:06 +02:00
renovate[bot]
b3545b60ea Update dependency vite to ^5.3.4 2024-07-16 13:43:12 +00:00
Jérémie Panzer
e6da3f693d Merge pull request #1488 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.25.0
2024-07-16 15:42:04 +02:00
renovate[bot]
4ab09da434 Update dependency react-router-dom to ^6.25.0 2024-07-16 13:28:04 +00:00
renovate[bot]
5e8daf29bf Update dependency vitest-mock-extended to ^1.3.2 2024-07-15 21:14:52 +00:00
Athou
024a1067bb update h2 2024-07-15 21:48:49 +02:00
renovate[bot]
c427da72b9 Update dependency vitest to ^2.0.3 2024-07-15 14:05:06 +00:00
Athou
346fb6b1ea release 4.6.0 2024-07-15 16:03:09 +02:00
Athou
1b658c76a3 show both read and unread entries when searching with keywords 2024-07-15 12:41:13 +02:00
Athou
1593ed62ba github actions is slow, increase timeout 2024-07-15 11:11:19 +02:00
Athou
085eddd4b0 fill the shared classes cache of openj9 even more 2024-07-15 10:57:26 +02:00
Jérémie Panzer
0db77ad2c0 Merge pull request #1487 from Athou/renovate/com.manticore-projects.tools-h2migrationtool-1.x
Update dependency com.manticore-projects.tools:h2migrationtool to v1.7
2024-07-15 10:41:13 +02:00
renovate[bot]
6f8bcb6c6a Update dependency com.manticore-projects.tools:h2migrationtool to v1.7 2024-07-15 07:43:51 +00:00
renovate[bot]
4196dee896 Lock file maintenance 2024-07-15 01:09:26 +00:00
Athou
6d49e0f0df build openj9 shared classes cache to improve startup time 2024-07-14 22:26:39 +02:00
Athou
d99f572989 move env variable definition before adding files in order to maximize layer reusability 2024-07-14 21:14:39 +02:00
Athou
fa197c33f1 rename field accordingly 2024-07-14 20:37:01 +02:00
Athou
1ce39a419e use "published" instead of "updated" (#1486) 2024-07-14 19:53:35 +02:00
Athou
f0e3ac8fcb README tweaks 2024-07-14 09:35:44 +02:00
renovate[bot]
30947cea05 Update mantine monorepo to ^7.11.2 2024-07-13 15:46:24 +00:00
Athou
9134f36d3b use openj9 as the Java runtime to reduce memory usage 2024-07-13 13:48:34 +02:00
Athou
dc526316a0 enable string deduplication to reduce memory usage 2024-07-13 10:25:32 +02:00
renovate[bot]
6593174668 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.3.1 2024-07-11 01:02:47 +00:00
renovate[bot]
0891c41abc Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.3.1 2024-07-10 22:28:34 +00:00
renovate[bot]
6ecb6254aa Update dependency npm to v10.8.2 2024-07-10 19:02:33 +00:00
renovate[bot]
84bd9eeeff Update dependency vitest to ^2.0.2 2024-07-10 16:49:16 +00:00
Jérémie Panzer
2549c4d47b Merge pull request #1485 from Athou/renovate/org.jsoup-jsoup-1.x
Update dependency org.jsoup:jsoup to v1.18.1
2024-07-10 13:16:58 +02:00
renovate[bot]
8750aa3dd6 Update dependency org.jsoup:jsoup to v1.18.1 2024-07-10 11:02:42 +00:00
Athou
262094a736 remove dangling comment 2024-07-10 08:55:45 +02:00
Jérémie Panzer
035201f917 Merge pull request #1483 from Athou/renovate/major-vitest-monorepo
Update dependency vitest to v2
2024-07-09 03:47:48 +02:00
Jérémie Panzer
ae9cbc5214 Merge pull request #1484 from Athou/renovate/node-20.15.x
Update dependency node to v20.15.1
2024-07-09 03:42:53 +02:00
Athou
78d5bf129a fix build 2024-07-09 03:42:38 +02:00
renovate[bot]
1f02ddd163 Update dependency node to v20.15.1 2024-07-08 20:17:38 +00:00
renovate[bot]
eff1e8cc7b Update dependency vitest to v2 2024-07-08 16:18:13 +00:00
Jérémie Panzer
dc8475b59a Merge pull request #1482 from Athou/renovate/lock-file-maintenance
Lock file maintenance
2024-07-08 07:05:27 +02:00
renovate[bot]
921968662d Lock file maintenance 2024-07-08 02:21:45 +00:00
251 changed files with 5006 additions and 5074 deletions

View File

@@ -1,6 +1 @@
# ignore everything
*
# allow only what we need
!commafeed-server/target/commafeed.jar
!commafeed-server/config.yml.example
commafeed-client

View File

@@ -1,121 +0,0 @@
name: Java CI
on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ "17", "21" ]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: "temurin"
cache: "maven"
# Build & Test
- name: Build with Maven
run: mvn --batch-mode --no-transfer-progress install
env:
TEST_DATABASE: h2
- name: Run integration tests on PostgreSQL
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
env:
TEST_DATABASE: postgresql
- name: Run integration tests on MySQL
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
env:
TEST_DATABASE: mysql
- name: Run integration tests on MariaDB
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
env:
TEST_DATABASE: mariadb
- name: Run integration tests with Redis cache enabled
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
env:
TEST_DATABASE: h2
REDIS: true
# Upload artifacts
- name: Upload JAR
uses: actions/upload-artifact@v4
if: ${{ matrix.java == '17' }}
with:
name: commafeed.jar
path: commafeed-server/target/commafeed.jar
- name: Upload Playwright artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: |
**/target/playwright-artifacts/
# Docker
- name: Login to Container Registry
uses: docker/login-action@v3
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag
uses: docker/build-push-action@v6
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm64/v8
tags: |
athou/commafeed:latest
athou/commafeed:${{ github.ref_name }}
- name: Docker build and push master
uses: docker/build-push-action@v6
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm64/v8
tags: athou/commafeed:master
# Create GitHub release after Docker image has been published
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: softprops/action-gh-release@v2
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}
draft: false
prerelease: false
files: |
commafeed-server/target/commafeed.jar
commafeed-server/config.yml.example

183
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
name: ci
on: [ push ]
env:
JAVA_VERSION: 21
DOCKER_BUILD_SUMMARY: false
jobs:
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
database: [ "h2", "postgresql", "mysql", "mariadb" ]
steps:
# Checkout
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
cache: "maven"
# Build & Test
- name: Build with Maven
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }}
# Upload artifacts
- name: Upload cross-platform app
uses: actions/upload-artifact@v4
with:
name: commafeed-${{ matrix.database }}-jvm
path: commafeed-server/target/commafeed-*.zip
- name: Upload native executable
uses: actions/upload-artifact@v4
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner
# Docker
- name: Login to Container Registry
uses: docker/login-action@v3
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
## tags
- name: Docker build and push tag - native
uses: docker/build-push-action@v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
push: true
platforms: linux/amd64
tags: |
athou/commafeed:latest-${{ matrix.database }}
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
push: true
platforms: linux/amd64,linux/arm64/v8
tags: |
athou/commafeed:latest-${{ matrix.database }}-jvm
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
## master
- name: Docker build and push master - native
uses: docker/build-push-action@v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
push: true
platforms: linux/amd64
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
push: true
platforms: linux/amd64,linux/arm64/v8
tags: athou/commafeed:master-${{ matrix.database }}-jvm
build-windows:
runs-on: windows-latest
strategy:
matrix:
database: [ "h2", "postgresql", "mysql", "mariadb" ]
steps:
# Checkout
- name: Configure git to checkout as-is
run: git config --global core.autocrlf false
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
cache: "maven"
# Build & Test
- name: Build with Maven
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }}
# Upload artifacts
- name: Upload native executable
uses: actions/upload-artifact@v4
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner.exe
release:
runs-on: ubuntu-latest
needs:
- build-linux
- build-windows
if: github.ref_type == 'tag'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: commafeed-*
path: ./artifacts
merge-multiple: true
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: ncipollo/release-action@v1
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}
artifacts: ./artifacts/*
- name: Update Docker Hub Description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: athou/commafeed
short-description: ${{ github.event.repository.description }}
readme-filepath: commafeed-server/src/main/docker/README.md

View File

@@ -15,4 +15,4 @@
# specific language governing permissions and limitations
# under the License.
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

View File

@@ -1,5 +1,47 @@
# Changelog
## [5.0.2]
- Fix favicon fetching for Youtube channels in native mode when Google auth key is set
- Fix an error that appears in the logs when fetching some favicons
## [5.0.1]
- Configure native compilation to support older CPU architectures (#1524)
## [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).
- CommaFeed now has a different package for each supported database.
- If you are deploying CommaFeed with a precompiled package, please
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
- If you are building CommaFeed from sources, please
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
- If you are using the Docker image, please read the instructions on
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
Please
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
- Use a different icon for filtering unread entries and marking an entry as read (#1506)
- Added various HTML attributes to ease custom JS/CSS customization (#1507)
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
needed, even for instances with a large number of feeds.
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
## [4.6.0]
- switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
- fix an issue that could cause old entries to reappear if they were updated by their author (#1486)
- show all entries regardless of their read status when searching with keywords, even if the ui is configured to show
unread entries only
## [4.5.0]
- significantly reduce the time needed to retrieve entries or mark them as read, especially when there are a lot of

View File

@@ -1,12 +0,0 @@
FROM eclipse-temurin:21.0.3_9-jre
EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar .
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]

111
README.md
View File

@@ -1,6 +1,6 @@
# CommaFeed
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
![preview](https://user-images.githubusercontent.com/1256795/184886828-1973f148-58a9-4c6d-9587-ee5e5d3cc2cb.png)
@@ -8,14 +8,22 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
- 4 different layouts
- Light/Dark theme
- Fully responsive
- Fully responsive, works great on both mobile and desktop
- Keyboard shortcuts for almost everything
- Support for right-to-left feeds
- Translated in 25+ languages
- Supports thousands of users and millions of feeds
- OPML import/export
- REST API and a Fever-compatible API for native mobile apps
- REST API
- Fever-compatible API for native mobile apps
- Can automatically mark articles as read based on user-defined rules
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
- Compiles to native code for blazing fast startup and low memory usage
- Supports 4 databases
- H2 (embedded database)
- PostgreSQL
- MySQL
- MariaDB
## Deployment
@@ -33,32 +41,80 @@ PikaPods shares 20% of the revenue back to CommaFeed.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed)
### Download precompiled package
### Download a precompiled package
mkdir commafeed && cd commafeed
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
system and database of choice.
The server will listen on http://localhost:8082. The default
user is `admin` and the default password is `admin`.
There are two types of packages:
- The `linux-x86_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
directly.
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
platforms and is started with `java -jar quarkus-run.jar`.
If available for your operating system, the native package is recommended because it has a faster startup time and lower
memory usage.
### Build from sources
git clone https://github.com/Athou/commafeed.git
cd commafeed
./mvnw clean package
cp commafeed-server/config.yml.example config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
./mvnw clean package [-P<database>] [-Pnative] [-DskipTests]
The server will listen on http://localhost:8082. The default
user is `admin` and the default password is `admin`.
- `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
variable pointing to a GraalVM installation).
- `-DskipTests` to speed up the build process by skipping tests.
### Memory management
When the build is complete:
- a zip containing all jars required to run the application is located at
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
`java -jar quarkus-run.jar`
- if you used the native profile, the executable is located at
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
## Configuration
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
the `data` directory of the current directory.
To use a different database, you will need to configure the following properties:
- `quarkus.datasource.jdbc.url`
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
- e.g. for MySQL:
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
- e.g. for MariaDB:
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
- `quarkus.datasource.username`
- `quarkus.datasource.password`
There are multiple ways to configure CommaFeed:
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
directory (keys in kebab-case)
- Command line arguments prefixed with `-D` (keys in kebab-case)
- Environment variables (keys in UPPER_CASE)
- a `.env` file in the working directory (keys in UPPER_CASE)
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
All [CommaFeed settings](commafeed-server/doc/commafeed.adoc) are optional and have sensible default values.
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
When started, the server will listen on http://localhost:8082.
The default user is `admin` and the default password is `admin`.
### Memory management (`jvm` package only)
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
However, this can be problematic on systems with limited memory.
This can be problematic on systems with limited memory.
#### Hard limit
@@ -67,16 +123,25 @@ For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
#### Dynamic sizing
The JVM can be configured to release unused memory to the operating system with the following parameters:
In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
following parameters:
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
-Xms20m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
This is how the Docker image is configured.
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
more
information.
#### OpenJ9
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
slightly slower throughput.
IBM provides precompiled binaries for OpenJ9
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile).
## Translation
Files for internationalization are
@@ -99,7 +164,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
- Open `commafeed-server` in your preferred Java IDE.
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
- run `./mvnw quarkus:dev`
### Frontend

View File

@@ -1,52 +0,0 @@
{
"locales": [
"ar",
"ca",
"cs",
"cy",
"da",
"de",
"en",
"es",
"fa",
"fi",
"fr",
"gl",
"hu",
"id",
"it",
"ja",
"ko",
"ms",
"nb",
"nl",
"nn",
"pl",
"pt",
"ru",
"sk",
"sv",
"tr",
"zh"
],
"catalogs": [
{
"path": "src/locales/{locale}/messages",
"include": [
"src"
],
"exclude": [
"src/locales/**"
]
}
],
"format": "po",
"formatOptions": {
"origins": true,
"lineNumbers": false
},
"sourceLocale": "en",
"fallbackLocales": {
"default": "en"
}
}

View File

@@ -0,0 +1,52 @@
import type { LinguiConfig } from "@lingui/conf"
const config: LinguiConfig = {
locales: [
"ar",
"ca",
"cs",
"cy",
"da",
"de",
"en",
"es",
"fa",
"fi",
"fr",
"gl",
"hu",
"id",
"it",
"ja",
"ko",
"ms",
"nb",
"nl",
"nn",
"pl",
"pt",
"ru",
"sk",
"sv",
"tr",
"zh",
],
catalogs: [
{
path: "src/locales/{locale}/messages",
include: ["src"],
exclude: ["src/locales/**"],
},
],
format: "po",
formatOptions: {
origins: true,
lineNumbers: false,
},
sourceLocale: "en",
fallbackLocales: {
default: "en",
},
}
export default config

File diff suppressed because it is too large Load Diff

View File

@@ -15,21 +15,21 @@
"i18n:extract": "lingui extract --clean"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@fontsource/open-sans": "^5.0.28",
"@lingui/core": "^4.11.2",
"@lingui/macro": "^4.11.2",
"@lingui/react": "^4.11.2",
"@mantine/core": "^7.11.1",
"@mantine/form": "^7.11.1",
"@mantine/hooks": "^7.11.1",
"@mantine/modals": "^7.11.1",
"@mantine/notifications": "^7.11.1",
"@mantine/spotlight": "^7.11.1",
"@emotion/react": "^11.13.0",
"@fontsource/open-sans": "^5.0.29",
"@lingui/core": "^4.11.3",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",
"@mantine/core": "^7.12.1",
"@mantine/form": "^7.12.1",
"@mantine/hooks": "^7.12.1",
"@mantine/modals": "^7.12.1",
"@mantine/notifications": "^7.12.1",
"@mantine/spotlight": "^7.12.1",
"@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.2.6",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"@reduxjs/toolkit": "^2.2.7",
"axios": "^1.7.4",
"dayjs": "^1.11.12",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.50.0",
@@ -42,23 +42,23 @@
"react-draggable": "^4.4.6",
"react-ga4": "^2.1.0",
"react-helmet": "^6.1.0",
"react-icons": "^5.2.1",
"react-icons": "^5.3.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.1.2",
"react-router-dom": "^6.24.1",
"react-router-dom": "^6.26.1",
"react-swipeable": "^7.0.1",
"redoc": "^2.1.5",
"throttle-debounce": "^5.0.2",
"tinycon": "^0.6.8",
"tss-react": "^4.9.10",
"tss-react": "^4.9.12",
"use-local-storage": "^3.0.0",
"vite-plugin-biome": "^1.0.12",
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@lingui/cli": "^4.11.2",
"@lingui/vite-plugin": "^4.11.2",
"@lingui/cli": "^4.11.3",
"@lingui/vite-plugin": "^4.11.3",
"@types/mousetrap": "^1.6.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@@ -70,10 +70,10 @@
"@vitejs/plugin-react": "^4.3.1",
"babel-plugin-macros": "^3.1.0",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-mock-extended": "^1.3.1"
"typescript": "^5.5.4",
"vite": "^5.4.1",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.0.5",
"vitest-mock-extended": "^2.0.0"
}
}

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>4.5.0</version>
<version>5.0.2</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v20.15.0</node.version>
<node.version>v20.16.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>10.8.1</npm.version>
<npm.version>10.8.2</npm.version>
</properties>
<build>
@@ -80,7 +80,7 @@
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
<outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
<resources>
<resource>
<directory>dist</directory>

View File

@@ -81,7 +81,17 @@ export const client = {
},
},
user: {
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
login: async (req: LoginRequest) => {
const formData = new URLSearchParams()
formData.append("j_username", req.name)
formData.append("j_password", req.password)
return await axiosInstance.post("j_security_check", formData, {
baseURL: ".",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
},
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),

View File

@@ -1,7 +1,7 @@
import { t } from "@lingui/macro"
import type { IconType } from "react-icons"
import { FaAt } from "react-icons/fa"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
import type { Category, Entry, SharingSettings } from "./types"
const categories: Record<string, Category> = {
@@ -50,10 +50,10 @@ const sharing: {
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
},
twitter: {
label: "Twitter",
icon: SiTwitter,
color: "#1D9BF0",
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`,
label: "X",
icon: SiX,
color: "#000000",
url: (url, desc) => `https://x.com/share?text=${desc}&url=${url}`,
},
tumblr: {
label: "Tumblr",

View File

@@ -5,7 +5,7 @@ import { type RootState, reducers } from "app/store"
import type { Entries, Entry } from "app/types"
import type { AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { mockReset } from "vitest-mock-extended"
import { any, mockReset } from "vitest-mock-extended"
const mockClient = await vi.hoisted(async () => {
const mockModule = await import("vitest-mock-extended")
@@ -19,7 +19,7 @@ describe("entries", () => {
})
it("loads entries", async () => {
mockClient.feed.getEntries.mockResolvedValue({
mockClient.feed.getEntries.calledWith(any()).mockResolvedValue({
data: {
entries: [{ id: "3" } as Entry],
hasMore: false,
@@ -53,7 +53,7 @@ describe("entries", () => {
})
it("loads more entries", async () => {
mockClient.category.getEntries.mockResolvedValue({
mockClient.category.getEntries.calledWith(any()).mockResolvedValue({
data: {
entries: [{ id: "4" } as Entry],
hasMore: false,

View File

@@ -40,7 +40,7 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
id: source.type === "tag" ? Constants.categories.all.id : source.id,
order: state.user.settings?.readingOrder,
readType: state.user.settings?.readingMode,
readType: state.entries.search ? "all" : state.user.settings?.readingMode,
offset,
limit: 50,
tag: source.type === "tag" ? source.id : undefined,

View File

@@ -1,3 +1,5 @@
import type { MessageDescriptor } from "@lingui/core"
import { useLingui } from "@lingui/react"
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
import { Constants } from "app/constants"
@@ -7,7 +9,7 @@ import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
interface ActionButtonProps {
className?: string
icon?: ReactNode
label: ReactNode
label?: string | MessageDescriptor
onClick?: MouseEventHandler
variant?: ActionIconVariant & ButtonVariant
hideLabelOnDesktop?: boolean
@@ -20,17 +22,35 @@ interface ActionButtonProps {
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const { mobile } = useActionButton()
const theme = useMantineTheme()
const { _ } = useLingui()
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)
return iconOnly ? (
<Tooltip label={props.label} openDelay={Constants.tooltip.delay}>
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
<ActionIcon
ref={ref}
color={theme.primaryColor}
variant={variant}
className={props.className}
onClick={props.onClick}
aria-label={label}
>
{props.icon}
</ActionIcon>
</Tooltip>
) : (
<Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}>
{props.label}
<Button
ref={ref}
variant={variant}
size="xs"
className={props.className}
leftSection={props.icon}
onClick={props.onClick}
aria-label={label}
>
{label}
</Button>
)
})

View File

@@ -150,9 +150,7 @@ export function KeyboardShortcutsHelp() {
<Trans>Navigate to a subscription by entering its name</Trans>
</Table.Td>
<Table.Td>
<Kbd>
<Trans>{isMacOS ? "Cmd" : "Ctrl"}</Trans>
</Kbd>
<Kbd>{isMacOS ? <Trans>Cmd</Trans> : <Trans>Ctrl</Trans>}</Kbd>
<span> + </span>
<Kbd>K</Kbd>
<span>, </span>

View File

@@ -305,11 +305,12 @@ export function FeedEntries() {
loader={<Box key={0}>{loading && <Loader />}</Box>}
>
{entries.map(entry => (
<div
<article
key={entry.id}
ref={el => {
if (el) el.id = Constants.dom.entryId(entry)
}}
data-id={entry.id}
>
<FeedEntry
entry={entry}
@@ -322,7 +323,7 @@ export function FeedEntries() {
onBodyClick={() => bodyClicked(entry)}
onSwipedLeft={async () => await swipedLeft(entry)}
/>
</div>
</article>
))}
</InfiniteScroll>
)

View File

@@ -9,7 +9,7 @@ import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useColorScheme } from "hooks/useColorScheme"
import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { tss } from "tss"
interface FeedEntryContextMenuProps {
@@ -70,7 +70,7 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
{props.entry.markable && (
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
<Group>
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
{props.entry.read ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
</Group>
</Item>

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
@@ -6,7 +7,7 @@ import type { Entry } from "app/types"
import { ActionButton } from "components/ActionButton"
import { useActionButton } from "hooks/useActionButton"
import { useMobile } from "hooks/useMobile"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps {
@@ -18,6 +19,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const mobile = useMobile()
const { spacing } = useActionButton()
const dispatch = useAppDispatch()
const { _ } = useLingui()
const readStatusButtonClicked = async () =>
await dispatch(
@@ -39,14 +41,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<Group gap={spacing}>
{props.entry.markable && (
<ActionButton
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
icon={props.entry.read ? <TbMail size={18} /> : <TbMailOpened size={18} />}
label={props.entry.read ? msg`Keep unread` : msg`Mark as read`}
onClick={readStatusButtonClicked}
/>
)}
<ActionButton
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
label={props.entry.starred ? msg`Unstar` : msg`Star`}
onClick={async () =>
await dispatch(
starEntry({
@@ -59,7 +61,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target>
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
<ActionButton icon={<TbShare size={18} />} label={msg`Share`} />
</Popover.Target>
<Popover.Dropdown>
<ShareButtons url={props.entry.url} description={props.entry.title} />
@@ -70,12 +72,12 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
<ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
</Indicator>
</Popover.Target>
<Popover.Dropdown>
<TagsInput
placeholder={t`Tags`}
placeholder={_(msg`Tags`)}
data={tags}
value={props.entry.tags}
onChange={onTagsChange}
@@ -88,13 +90,13 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)}
<a href={props.entry.url} target="_blank" rel="noreferrer">
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
<ActionButton icon={<TbExternalLink size={18} />} label={msg`Open link`} />
</a>
</Group>
<ActionButton
icon={<TbArrowBarToDown size={18} />}
label={<Trans>Mark as read up to here</Trans>}
label={msg`Mark as read up to here`}
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
/>
</Group>

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
@@ -13,6 +14,7 @@ import { CategorySelect } from "./CategorySelect"
export function AddCategory() {
const dispatch = useAppDispatch()
const { _ } = useLingui()
const form = useForm<AddCategoryRequest>()
@@ -33,7 +35,7 @@ export function AddCategory() {
<form onSubmit={form.onSubmit(addCategory.execute)}>
<Stack>
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
<TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
<Group justify="center">
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>

View File

@@ -1,4 +1,5 @@
import { t } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Select, type SelectProps } from "@mantine/core"
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants"
@@ -13,6 +14,8 @@ type CategorySelectProps = Partial<SelectProps> & {
export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const { _ } = useLingui()
const categories = rootCategory && flattenCategoryTree(rootCategory)
const categoriesById = categories?.reduce((map, c) => {
map.set(c.id, c)
@@ -43,7 +46,7 @@ export function CategorySelect(props: CategorySelectProps) {
.sort((c1, c2) => c1.label.localeCompare(c2.label))
if (props.withAll) {
selectData?.unshift({
label: t`All`,
label: _(msg`All`),
value: Constants.categories.all.id,
})
}

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
import { isNotEmpty, useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
@@ -11,10 +12,11 @@ import { TbFileImport } from "react-icons/tb"
export function ImportOpml() {
const dispatch = useAppDispatch()
const { _ } = useLingui()
const form = useForm<{ file: File }>({
validate: {
file: isNotEmpty(t`OPML file is required`),
file: isNotEmpty(_(msg`OPML file is required`)),
},
})
@@ -38,7 +40,7 @@ export function ImportOpml() {
<FileInput
label={<Trans>OPML file</Trans>}
leftSection={<TbFileImport />}
placeholder={t`OPML file`}
placeholder={_(msg`OPML file`)}
description={
<Trans>
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
@@ -57,10 +58,11 @@ export function Header() {
const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
const dispatch = useAppDispatch()
const { _ } = useLingui()
const searchForm = useForm<{ search: string }>({
validate: {
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
search: value => (value.length > 0 && value.length < 3 ? _(msg`Search requires at least 3 characters`) : null),
},
})
const { setValues } = searchForm
@@ -77,7 +79,7 @@ export function Header() {
<HeaderToolbar>
<ActionButton
icon={<TbArrowUp size={iconSize} />}
label={<Trans>Previous</Trans>}
label={msg`Previous`}
onClick={async () =>
await dispatch(
selectPreviousEntry({
@@ -90,7 +92,7 @@ export function Header() {
/>
<ActionButton
icon={<TbArrowDown size={iconSize} />}
label={<Trans>Next</Trans>}
label={msg`Next`}
onClick={async () =>
await dispatch(
selectNextEntry({
@@ -106,7 +108,7 @@ export function Header() {
<ActionButton
icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>}
label={msg`Refresh`}
onClick={async () => await dispatch(reloadEntries())}
/>
<MarkAllAsReadButton iconSize={iconSize} />
@@ -115,25 +117,25 @@ export function Header() {
<ActionButton
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/>
<ActionButton
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/>
<Popover>
<Popover.Target>
<Indicator disabled={!searchFromStore}>
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} />
<ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} />
</Indicator>
</Popover.Target>
<Popover.Dropdown>
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
<TextInput
placeholder={t`Search`}
placeholder={_(msg`Search`)}
{...searchForm.getInputProps("search")}
leftSection={<TbSearch size={iconSize} />}
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
@@ -153,12 +155,12 @@ export function Header() {
<ActionButton
icon={<TbSettings size={iconSize} />}
label={<Trans>Extension options</Trans>}
label={msg`Extension options`}
onClick={() => openSettingsPage()}
/>
<ActionButton
icon={<TbExternalLink size={iconSize} />}
label={<Trans>Open CommaFeed</Trans>}
label={msg`Open CommaFeed`}
onClick={() => openAppInNewTab()}
/>
</>

View File

@@ -1,4 +1,4 @@
import { Trans } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/entries/thunks"
@@ -91,7 +91,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
</Group>
</Stack>
</Modal>
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
<ActionButton icon={<TbChecks size={props.iconSize} />} label={msg`Mark all as read`} onClick={buttonClicked} />
</>
)
}

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants"
@@ -33,6 +34,7 @@ export function DisplaySettings() {
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch()
const { _ } = useLingui()
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
always: <Trans>Always</Trans>,
@@ -43,19 +45,19 @@ export function DisplaySettings() {
const displayModeData: ComboboxData = [
{
value: "always",
label: t`Always`,
label: _(msg`Always`),
},
{
value: "on_desktop",
label: t`On desktop`,
label: _(msg`On desktop`),
},
{
value: "on_mobile",
label: t`On mobile`,
label: _(msg`On mobile`),
},
{
value: "never",
label: t`Never`,
label: _(msg`Never`),
},
]

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals"
@@ -19,10 +20,11 @@ interface FormData extends ProfileModificationRequest {
export function ProfileSettings() {
const profile = useAppSelector(state => state.user.profile)
const dispatch = useAppDispatch()
const { _ } = useLingui()
const form = useForm<FormData>({
validate: {
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? _(msg`Passwords do not match`) : null),
},
})
const { setValues } = form

View File

@@ -70,6 +70,7 @@ export function Tree() {
const allCategoryNode = () => (
<TreeNode
id={Constants.categories.all.id}
type="category"
name={<Trans>All</Trans>}
icon={allIcon}
unread={categoryUnreadCount(root)}
@@ -83,6 +84,7 @@ export function Tree() {
const starredCategoryNode = () => (
<TreeNode
id={Constants.categories.starred.id}
type="category"
name={<Trans>Starred</Trans>}
icon={starredIcon}
unread={0}
@@ -102,6 +104,7 @@ export function Tree() {
return (
<TreeNode
id={category.id}
type="category"
name={category.name}
icon={category.expanded ? expandedIcon : collapsedIcon}
unread={unreadCount}
@@ -122,6 +125,7 @@ export function Tree() {
return (
<TreeNode
id={String(feed.id)}
type="feed"
name={feed.name}
icon={feed.iconUrl}
unread={feed.unread}
@@ -137,6 +141,7 @@ export function Tree() {
const tagNode = (tag: string) => (
<TreeNode
id={tag}
type="tag"
name={tag}
icon={tagIcon}
unread={0}

View File

@@ -1,4 +1,5 @@
import { Box, Center } from "@mantine/core"
import type { EntrySourceType } from "app/entries/slice"
import { FeedFavicon } from "components/content/FeedFavicon"
import type React from "react"
import { tss } from "tss"
@@ -6,6 +7,7 @@ import { UnreadCount } from "./UnreadCount"
interface TreeNodeProps {
id: string
type: EntrySourceType
name: React.ReactNode
icon: React.ReactNode
unread: number
@@ -63,7 +65,15 @@ export function TreeNode(props: TreeNodeProps) {
hasUnread: props.unread > 0,
})
return (
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
<Box
py={1}
pl={props.level * 20}
className={classes.node}
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
data-id={props.id}
data-type={props.type}
data-unread-count={props.unread}
>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
</Box>

View File

@@ -1,6 +1,6 @@
import { Trans, t } from "@lingui/macro"
import { Box, Center, Kbd, TextInput } from "@mantine/core"
import { useOs } from "@mantine/hooks"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { TextInput } from "@mantine/core"
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch } from "app/store"
@@ -15,7 +15,8 @@ export interface TreeSearchProps {
export function TreeSearch(props: TreeSearchProps) {
const dispatch = useAppDispatch()
const isMacOS = useOs() === "macos"
const { _ } = useLingui()
const actions: SpotlightActionData[] = props.feeds
.map(f => ({
id: `${f.id}`,
@@ -26,13 +27,6 @@ export function TreeSearch(props: TreeSearchProps) {
.sort((f1, f2) => f1.label.localeCompare(f2.label))
const searchIcon = <TbSearch size={18} />
const rightSection = (
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
<Kbd>{isMacOS ? "Cmd" : "Ctrl"}</Kbd>
<Box mx={5}>+</Box>
<Kbd>K</Kbd>
</Center>
)
// additional keyboard shortcut used by commafeed v1
useMousetrap("g u", () => spotlight.open())
@@ -40,10 +34,9 @@ export function TreeSearch(props: TreeSearchProps) {
return (
<>
<TextInput
placeholder={t`Search`}
placeholder={_(msg`Search`)}
leftSection={searchIcon}
rightSectionWidth={100}
rightSection={rightSection}
styles={{
input: {
cursor: "pointer",
@@ -60,7 +53,7 @@ export function TreeSearch(props: TreeSearchProps) {
shortcut="mod+k"
searchProps={{
leftSection: searchIcon,
placeholder: t`Search`,
placeholder: _(msg`Search`),
}}
nothingFound={<Trans>Nothing found</Trans>}
/>

View File

@@ -18,7 +18,7 @@ export function UnreadCount(props: { unreadCount: number }) {
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
<Badge className={classes.badge} variant="light">
<Badge className={classes.badge} variant="light" fullWidth>
{count}
</Badge>
</Tooltip>

View File

@@ -1,4 +1,5 @@
import { t } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { useAppSelector } from "app/store"
interface Step {
@@ -11,22 +12,23 @@ export const useAppLoading = () => {
const settings = useAppSelector(state => state.user.settings)
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const tags = useAppSelector(state => state.user.tags)
const { _ } = useLingui()
const steps: Step[] = [
{
label: t`Loading settings...`,
label: _(msg`Loading settings...`),
done: !!settings,
},
{
label: t`Loading profile...`,
label: _(msg`Loading profile...`),
done: !!profile,
},
{
label: t`Loading subscriptions...`,
label: _(msg`Loading subscriptions...`),
done: !!rootCategory,
},
{
label: t`Loading tags...`,
label: _(msg`Loading tags...`),
done: !!tags,
},
]

View File

@@ -10,7 +10,7 @@ interface Locale {
}
// add an object to the array to add a new locale
// don't forget to also add it to the 'locales' array in .linguirc
// don't forget to also add it to the 'locales' array in lingui.config.ts
export const locales: Locale[] = [
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },

View File

@@ -176,6 +176,10 @@ msgstr "تأكد من عمل الخلاصة"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Comproveu que el canal funciona"
msgid "Close menu"
msgstr "Tanca el menu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Tjek, at foderet virker"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Überprüfe, ob der Feed funktioniert"
msgid "Close menu"
msgstr "Menü schließen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "Check that the feed is working"
msgid "Close menu"
msgstr "Close menu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr "Cmd"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed browser extension version {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "Compruebe que el feed funciona"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "بررسی کنید که خوراک کار می کند"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Tarkista, että syöttö toimii"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Vérifie que le flux fonctionne"
msgid "Close menu"
msgstr "Fermer le menu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "Comproba que a fonte funciona"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Ellenőrizze, hogy a feed működik-e"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Periksa apakah umpannya berfungsi"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Verifica che il feed funzioni"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "フィードが動作していることを確認してください"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "피드가 작동하는지 확인"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Semak sama ada suapan berfungsi"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Controleer of de feed werkt"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Sprawdź, czy kanał działa"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Verifique se o feed está funcionando"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Проверьте, работает ли лента."
msgid "Close menu"
msgstr "Закрыть меню"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Версия расширения браузера CommaFeed {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "Skontrolujte, či feed funguje"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Kontrollera att matningen fungerar"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""

View File

@@ -176,6 +176,10 @@ msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
msgid "Close menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."

View File

@@ -176,6 +176,10 @@ msgstr "检查信息流是否正常工作"
msgid "Close menu"
msgstr "关闭菜单"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed的浏览器扩展版本{browserExtensionVersion}。"

View File

@@ -1,4 +1,4 @@
import { Trans } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, useMantineColorScheme } from "@mantine/core"
import { client } from "app/client"
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
@@ -9,7 +9,7 @@ import { ActionButton } from "components/ActionButton"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useAsyncCallback } from "react-async-hook"
import { SiGithub, SiTwitter } from "react-icons/si"
import { SiGithub, SiX } from "react-icons/si"
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
import { PageTitle } from "./PageTitle"
@@ -38,7 +38,7 @@ export function WelcomePage() {
{serverInfos?.demoAccountEnabled && (
<Center>
<ActionButton
label={<Trans>Try the demo!</Trans>}
label={msg`Try the demo!`}
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={async () => await login.execute({ name: "demo", password: "demo" })}
@@ -96,7 +96,7 @@ function Buttons() {
return (
<Group gap={14}>
<ActionButton
label={<Trans>Log in</Trans>}
label={msg`Log in`}
icon={<TbKey size={iconSize} />}
variant="outline"
onClick={async () => await dispatch(redirectToLogin())}
@@ -104,7 +104,7 @@ function Buttons() {
/>
{serverInfos?.allowRegistrations && (
<ActionButton
label={<Trans>Sign up</Trans>}
label={msg`Sign up`}
icon={<TbUserPlus size={iconSize} />}
variant="filled"
onClick={async () => await dispatch(redirectToRegistration())}
@@ -113,7 +113,7 @@ function Buttons() {
)}
<ActionButton
label={dark ? <Trans>Switch to light theme</Trans> : <Trans>Switch to dark theme</Trans>}
label={dark ? msg`Switch to light theme` : msg`Switch to dark theme`}
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
onClick={() => toggleColorScheme()}
hideLabelOnDesktop
@@ -121,7 +121,7 @@ function Buttons() {
{isBrowserExtensionPopup && (
<ActionButton
label={<Trans>Extension options</Trans>}
label={msg`Extension options`}
icon={<TbSettings size={iconSize} />}
onClick={() => openSettingsPage()}
hideLabelOnDesktop
@@ -140,8 +140,8 @@ function Footer() {
<Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer">
<SiGithub />
</Anchor>
<Anchor variant="text" href="https://twitter.com/CommaFeed" target="_blank" rel="noreferrer">
<SiTwitter />
<Anchor variant="text" href="https://x.com/CommaFeed" target="_blank" rel="noreferrer">
<SiX />
</Anchor>
</Group>
<Box>

View File

@@ -1,74 +1,63 @@
import { Accordion, Box, Tabs } from "@mantine/core"
import { Accordion, Box } from "@mantine/core"
import { client } from "app/client"
import { Loader } from "components/Loader"
import { Gauge } from "components/metrics/Gauge"
import { Meter } from "components/metrics/Meter"
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
import { Timer } from "components/metrics/Timer"
import { useEffect } from "react"
import { useAsync } from "react-async-hook"
import { TbChartAreaLine, TbClock } from "react-icons/tb"
const shownMeters: Record<string, string> = {
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryInserted": "Entries inserted",
"com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted",
}
const shownGauges: Record<string, string> = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Feed Refresh Engine queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Refresh Engine active HTTP workers",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Refresh Engine active database update workers",
"com.commafeed.backend.HttpGetter.pool.max": "HttpGetter max pool size",
"com.commafeed.backend.HttpGetter.pool.size": "HttpGetter current pool size",
"com.commafeed.backend.HttpGetter.pool.leased": "HttpGetter active connections",
"com.commafeed.backend.HttpGetter.pool.pending": "HttpGetter waiting for a connection",
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",
"com.commafeed.frontend.ws.WebSocketSessions.sessions": "WebSocket sessions",
}
export function MetricsPage() {
const query = useAsync(async () => await client.admin.getMetrics(), [])
const query = useAsync(async () => await client.admin.getMetrics(), [], {
// keep previous results available while a new request is pending
setLoading: state => ({ ...state, loading: true }),
})
useEffect(() => {
const interval = setInterval(() => query.execute(), 2000)
return () => clearInterval(interval)
}, [query.execute])
if (!query.result) return <Loader />
const { meters, gauges, timers } = query.result.data
const { meters, gauges } = query.result.data
return (
<Tabs defaultValue="stats">
<Tabs.List>
<Tabs.Tab value="stats" leftSection={<TbChartAreaLine size={14} />}>
Stats
</Tabs.Tab>
<Tabs.Tab value="timers" leftSection={<TbClock size={14} />}>
Timers
</Tabs.Tab>
</Tabs.List>
<>
<Accordion variant="contained" chevronPosition="left">
{Object.keys(shownMeters).map(m => (
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
<Meter meter={meters[m]} />
</MetricAccordionItem>
))}
</Accordion>
<Tabs.Panel value="stats" pt="xs">
<Accordion variant="contained" chevronPosition="left">
{Object.keys(shownMeters).map(m => (
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
<Meter meter={meters[m]} />
</MetricAccordionItem>
))}
</Accordion>
<Box pt="xs">
{Object.keys(shownGauges).map(g => (
<Box key={g}>
<span>{shownGauges[g]}&nbsp;</span>
<Gauge gauge={gauges[g]} />
</Box>
))}
</Box>
</Tabs.Panel>
<Tabs.Panel value="timers" pt="xs">
<Accordion variant="contained" chevronPosition="left">
{Object.keys(timers).map(key => (
<MetricAccordionItem key={key} metricKey={key} name={key} headerValue={timers[key].count}>
<Timer timer={timers[key]} />
</MetricAccordionItem>
))}
</Accordion>
</Tabs.Panel>
</Tabs>
<Box pt="xs">
{Object.keys(shownGauges).map(g => (
<Box key={g}>
<span>{shownGauges[g]}:&nbsp;</span>
<Gauge gauge={gauges[g]} />
</Box>
))}
</Box>
</>
)
}

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
import { Constants } from "app/constants"
import { redirectToApiDocumentation } from "app/redirect/thunks"
@@ -36,6 +37,8 @@ function Section(props: { title: React.ReactNode; icon: React.ReactNode; childre
function NextUnreadBookmarklet() {
const [categoryId, setCategoryId] = useState(Constants.categories.all.id)
const [order, setOrder] = useState("desc")
const { _ } = useLingui()
const baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf("#"))
const href = `javascript:window.location.href='${baseUrl}next?category=${categoryId}&order=${order}&t='+new Date().getTime();`
@@ -44,8 +47,8 @@ function NextUnreadBookmarklet() {
<CategorySelect value={categoryId} onChange={c => c && setCategoryId(c)} withAll description={<Trans>Category</Trans>} />
<NativeSelect
data={[
{ value: "desc", label: t`Newest first` },
{ value: "asc", label: t`Oldest first` },
{ value: "desc", label: _(msg`Newest first`) },
{ value: "asc", label: _(msg`Oldest first`) },
]}
value={order}
onChange={e => setOrder(e.target.value)}

View File

@@ -80,7 +80,7 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
if (noSubscriptions) return <NoSubscriptionHelp />
return (
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
<Box mb={viewport.height * 0.75}>
<Box mb={viewport.height * 0.7}>
<Group gap="xl">
{sourceWebsiteUrl && (
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>

View File

@@ -1,4 +1,4 @@
import { Trans } from "@lingui/macro"
import { msg } from "@lingui/macro"
import { ActionIcon, AppShell, Box, Center, Group, ScrollArea, Title, useMantineTheme } from "@mantine/core"
import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
@@ -101,7 +101,7 @@ export default function Layout(props: LayoutProps) {
const burger = (
<ActionButton
label={mobileMenuOpen ? <Trans>Close menu</Trans> : <Trans>Open menu</Trans>}
label={mobileMenuOpen ? msg`Close menu` : msg`Open menu`}
icon={mobileMenuOpen ? <TbX size={18} /> : <TbMenu2 size={18} />}
onClick={() => dispatch(setMobileMenuOpen(!mobileMenuOpen))}
/>

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
@@ -13,6 +14,7 @@ import { Link } from "react-router-dom"
export function LoginPage() {
const serverInfos = useAppSelector(state => state.server.serverInfos)
const dispatch = useAppDispatch()
const { _ } = useLingui()
const form = useForm<LoginRequest>({
initialValues: {
@@ -43,7 +45,7 @@ export function LoginPage() {
<Stack>
<TextInput
label={<Trans>User Name or E-mail</Trans>}
placeholder={t`User Name or E-mail`}
placeholder={_(msg`User Name or E-mail`)}
{...form.getInputProps("name")}
description={
serverInfos?.demoAccountEnabled ? <Trans>Try out CommaFeed with the demo account: demo/demo</Trans> : ""
@@ -54,7 +56,7 @@ export function LoginPage() {
/>
<PasswordInput
label={<Trans>Password</Trans>}
placeholder={t`Password`}
placeholder={_(msg`Password`)}
{...form.getInputProps("password")}
size="md"
required

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
@@ -11,6 +12,7 @@ import { Link } from "react-router-dom"
export function PasswordRecoveryPage() {
const [message, setMessage] = useState("")
const { _ } = useLingui()
const form = useForm<PasswordResetRequest>({
initialValues: {
@@ -20,7 +22,7 @@ export function PasswordRecoveryPage() {
const recoverPassword = useAsyncCallback(client.user.passwordReset, {
onSuccess: () => {
setMessage(t`An email has been sent if this address was registered. Check your inbox.`)
setMessage(_(msg`An email has been sent if this address was registered. Check your inbox.`))
},
})
@@ -54,7 +56,7 @@ export function PasswordRecoveryPage() {
<TextInput
type="email"
label={<Trans>E-mail</Trans>}
placeholder={t`E-mail`}
placeholder={_(msg`E-mail`)}
{...form.getInputProps("email")}
size="md"
required

View File

@@ -1,4 +1,5 @@
import { Trans, t } from "@lingui/macro"
import { Trans, msg } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
@@ -13,6 +14,7 @@ import { Link } from "react-router-dom"
export function RegistrationPage() {
const serverInfos = useAppSelector(state => state.server.serverInfos)
const dispatch = useAppDispatch()
const { _ } = useLingui()
const form = useForm<RegistrationRequest>({
initialValues: {
@@ -22,12 +24,18 @@ export function RegistrationPage() {
},
})
const register = useAsyncCallback(client.user.register, {
const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
const register = useAsyncCallback(client.user.register, {
onSuccess: () => {
login.execute(form.values)
},
})
return (
<Container size="xs">
<PageTitle />
@@ -37,7 +45,7 @@ export function RegistrationPage() {
</Title>
{serverInfos && !serverInfos.allowRegistrations && (
<Box mb="md">
<Alert messages={[t`Registrations are closed on this CommaFeed instance`]} />
<Alert messages={[_(msg`Registrations are closed on this CommaFeed instance`)]} />
</Box>
)}
{serverInfos?.allowRegistrations && (
@@ -48,25 +56,31 @@ export function RegistrationPage() {
</Box>
)}
{login.error && (
<Box mb="md">
<Alert messages={errorToStrings(login.error)} />
</Box>
)}
<form onSubmit={form.onSubmit(register.execute)}>
<Stack>
<TextInput label="User Name" placeholder="User Name" {...form.getInputProps("name")} size="md" required />
<TextInput
type="email"
label={<Trans>E-mail address</Trans>}
placeholder={t`E-mail address`}
placeholder={_(msg`E-mail address`)}
{...form.getInputProps("email")}
size="md"
required
/>
<PasswordInput
label={<Trans>Password</Trans>}
placeholder={t`Password`}
placeholder={_(msg`Password`)}
{...form.getInputProps("password")}
size="md"
required
/>
<Button type="submit" loading={register.loading}>
<Button type="submit" loading={register.loading || login.loading}>
<Trans>Sign up</Trans>
</Button>
<Center>

View File

@@ -15,12 +15,11 @@ export default defineConfig(env => ({
},
}),
lingui(),
// https://github.com/vitest-dev/vitest/issues/4055#issuecomment-1732994672
tsconfigPaths(),
visualizer(),
biomePlugin({
mode: "check",
failOnError: true,
failOnError: env.mode !== "development",
}),
],
base: "./",
@@ -33,6 +32,7 @@ export default defineConfig(env => ({
"/openapi.json": "http://localhost:8083",
"/custom_css.css": "http://localhost:8083",
"/custom_js.js": "http://localhost:8083",
"/j_security_check": "http://localhost:8083",
"/logout": "http://localhost:8083",
},
},

View File

@@ -1,154 +0,0 @@
# CommaFeed settings
# ------------------
app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: true
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts
createDemoAccount: true
# put your google analytics tracking code here
googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads
backgroundThreads: 3
# number of database updating threads
databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery)
smtpHost: localhost
smtpPort: 25
smtpTls: false
smtpUserName: user
smtpPassword: pass
smtpFromAddress:
# Graphite Metric settings
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
graphiteEnabled: false
graphitePrefix: "test.commafeed"
graphiteHost: "localhost"
graphitePort: 2003
graphiteInterval: 60
# whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
# useful if commafeed is usually accessed through a restricting proxy
imageProxyEnabled: true
# database query timeout (in milliseconds), 0 to disable
queryTimeout: 0
# time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
# entries older than this will be deleted, 0 to disable
maxEntriesAgeDays: 365
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis'
cache: noop
# announcement string displayed on the main page
announcement:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
websocketEnabled: true
# interval at which the client will send a ping message on the websocket to keep the connection alive
websocketPingInterval: 15m
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
treeReloadInterval: 30s
# Database connection
# -------------------
# for MariaDB
# driverClass is org.mariadb.jdbc.Driver
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
#
# for MySQL
# driverClass is com.mysql.cj.jdbc.Driver
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
#
# for PostgreSQL
# driverClass is org.postgresql.Driver
# url is jdbc:postgresql://localhost:5432/commafeed
database:
driverClass: org.h2.Driver
url: jdbc:h2:./target/commafeed
user: sa
password: sa
properties:
charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
server:
applicationConnectors:
- type: http
port: 8083
adminConnectors:
- type: http
port: 8084
logging:
level: INFO
loggers:
com.commafeed: DEBUG
liquibase: INFO
org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
appenders:
- type: console
- type: file
currentLogFilename: log/commafeed.log
threshold: ALL
archive: true
archivedLogFilenamePattern: log/commafeed-%d.log
archivedFileCount: 5
timeZone: UTC
# Redis pool configuration
# (only used if app.cache is 'redis')
# -----------------------------------
redis:
host: localhost
port: 6379
# username is only required when using ACLs
username:
password:
timeout: 2000
database: 0
maxTotal: 500

View File

@@ -1,155 +0,0 @@
# CommaFeed settings
# ------------------
app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: false
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts
createDemoAccount: false
# put your google analytics tracking code here
googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads
backgroundThreads: 3
# number of database updating threads
databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery)
smtpHost:
smtpPort:
smtpTls: false
smtpUserName:
smtpPassword:
smtpFromAddress:
# Graphite Metric settings
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
graphiteEnabled: false
graphitePrefix: "test.commafeed"
graphiteHost: "localhost"
graphitePort: 2003
graphiteInterval: 60
# whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
# useful if commafeed is usually accessed through a restricting proxy
imageProxyEnabled: false
# database query timeout (in milliseconds), 0 to disable
queryTimeout: 0
# time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
# entries older than this will be deleted, 0 to disable
maxEntriesAgeDays: 365
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis'
cache: noop
# announcement string displayed on the main page
announcement:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
websocketEnabled: true
# interval at which the client will send a ping message on the websocket to keep the connection alive
websocketPingInterval: 15m
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
treeReloadInterval: 30s
# Database connection
# -------------------
# for MariaDB
# driverClass is org.mariadb.jdbc.Driver
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
#
# for MySQL
# driverClass is com.mysql.cj.jdbc.Driver
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
#
# for PostgreSQL
# driverClass is org.postgresql.Driver
# url is jdbc:postgresql://localhost:5432/commafeed
database:
driverClass: org.h2.Driver
url: jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE
user: sa
password: sa
properties:
charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
minSize: 1
maxSize: 50
maxConnectionAge: 30m
server:
applicationConnectors:
- type: http
port: 8082
adminConnectors: [ ]
requestLog:
appenders: [ ]
logging:
level: ERROR
loggers:
com.commafeed: INFO
liquibase: INFO
io.dropwizard.server.ServerFactory: INFO
appenders:
- type: console
- type: file
currentLogFilename: log/commafeed.log
threshold: ALL
archive: true
archivedLogFilenamePattern: log/commafeed-%d.log
archivedFileCount: 5
timeZone: UTC
# Redis pool configuration
# (only used if app.cache is 'redis')
# -----------------------------------
redis:
host: localhost
port: 6379
# username is only required when using ACLs
username:
password:
timeout: 2000
database: 0
maxTotal: 500

View File

@@ -0,0 +1,597 @@
:summaryTableId: commafeed
[.configuration-legend]
icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime
[.configuration-reference.searchable, cols="80,.^10,.^10"]
|===
h|[[commafeed_configuration]]link:#commafeed_configuration[Configuration property]
h|Type
h|Default
a| [[commafeed_commafeed-hide-from-web-crawlers]]`link:#commafeed_commafeed-hide-from-web-crawlers[commafeed.hide-from-web-crawlers]`
[.description]
--
Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`true`
a| [[commafeed_commafeed-image-proxy-enabled]]`link:#commafeed_commafeed-image-proxy-enabled[commafeed.image-proxy-enabled]`
[.description]
--
If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser. This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_IMAGE_PROXY_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_IMAGE_PROXY_ENABLED+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`
a| [[commafeed_commafeed-password-recovery-enabled]]`link:#commafeed_commafeed-password-recovery-enabled[commafeed.password-recovery-enabled]`
[.description]
--
Enable password recovery via email. Quarkus mailer will need to be configured.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`
a| [[commafeed_commafeed-announcement]]`link:#commafeed_commafeed-announcement[commafeed.announcement]`
[.description]
--
Message displayed in a notification at the bottom of the page.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_ANNOUNCEMENT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_ANNOUNCEMENT+++`
endif::add-copy-button-to-env-var[]
--|string
|
a| [[commafeed_commafeed-google-analytics-tracking-code]]`link:#commafeed_commafeed-google-analytics-tracking-code[commafeed.google-analytics-tracking-code]`
[.description]
--
Google Analytics tracking code.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++`
endif::add-copy-button-to-env-var[]
--|string
|
a| [[commafeed_commafeed-google-auth-key]]`link:#commafeed_commafeed-google-auth-key[commafeed.google-auth-key]`
[.description]
--
Google Auth key for fetching Youtube channel favicons.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_AUTH_KEY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_GOOGLE_AUTH_KEY+++`
endif::add-copy-button-to-env-var[]
--|string
|
a| [[commafeed_commafeed-http-client-user-agent]]`link:#commafeed_commafeed-http-client-user-agent[commafeed.http-client.user-agent]`
[.description]
--
User-Agent string that will be used by the http client, leave empty for the default one.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++`
endif::add-copy-button-to-env-var[]
--|string
|
a| [[commafeed_commafeed-http-client-connect-timeout]]`link:#commafeed_commafeed-http-client-connect-timeout[commafeed.http-client.connect-timeout]`
[.description]
--
Time to wait for a connection to be established.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5S`
a| [[commafeed_commafeed-http-client-ssl-handshake-timeout]]`link:#commafeed_commafeed-http-client-ssl-handshake-timeout[commafeed.http-client.ssl-handshake-timeout]`
[.description]
--
Time to wait for SSL handshake to complete.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5S`
a| [[commafeed_commafeed-http-client-socket-timeout]]`link:#commafeed_commafeed-http-client-socket-timeout[commafeed.http-client.socket-timeout]`
[.description]
--
Time to wait between two packets before timeout.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`10S`
a| [[commafeed_commafeed-http-client-response-timeout]]`link:#commafeed_commafeed-http-client-response-timeout[commafeed.http-client.response-timeout]`
[.description]
--
Time to wait for the full response to be received.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`10S`
a| [[commafeed_commafeed-http-client-connection-time-to-live]]`link:#commafeed_commafeed-http-client-connection-time-to-live[commafeed.http-client.connection-time-to-live]`
[.description]
--
Time to live for a connection in the pool.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`30S`
a| [[commafeed_commafeed-http-client-idle-connections-eviction-interval]]`link:#commafeed_commafeed-http-client-idle-connections-eviction-interval[commafeed.http-client.idle-connections-eviction-interval]`
[.description]
--
Time between eviction runs for idle connections.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`1M`
a| [[commafeed_commafeed-http-client-max-response-size]]`link:#commafeed_commafeed-http-client-max-response-size[commafeed.http-client.max-response-size]`
[.description]
--
If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++`
endif::add-copy-button-to-env-var[]
--|MemorySize link:#memory-size-note-anchor[icon:question-circle[title=More information about the MemorySize format]]
|`5M`
a| [[commafeed_commafeed-feed-refresh-interval]]`link:#commafeed_commafeed-feed-refresh-interval[commafeed.feed-refresh.interval]`
[.description]
--
Amount of time CommaFeed will wait before refreshing the same feed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5M`
a| [[commafeed_commafeed-feed-refresh-interval-empirical]]`link:#commafeed_commafeed-feed-refresh-interval-empirical[commafeed.feed-refresh.interval-empirical]`
[.description]
--
If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the last entry was published. The interval will be somewhere between the default refresh interval and 24h. See `FeedRefreshIntervalCalculator` for details.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`
a| [[commafeed_commafeed-feed-refresh-http-threads]]`link:#commafeed_commafeed-feed-refresh-http-threads[commafeed.feed-refresh.http-threads]`
[.description]
--
Amount of http threads used to fetch feeds.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++`
endif::add-copy-button-to-env-var[]
--|@jakarta.validation.constraints.Min(1L) int
|`3`
a| [[commafeed_commafeed-feed-refresh-database-threads]]`link:#commafeed_commafeed-feed-refresh-database-threads[commafeed.feed-refresh.database-threads]`
[.description]
--
Amount of threads used to insert new entries in the database.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++`
endif::add-copy-button-to-env-var[]
--|@jakarta.validation.constraints.Min(1L) int
|`1`
a| [[commafeed_commafeed-feed-refresh-user-inactivity-period]]`link:#commafeed_commafeed-feed-refresh-user-inactivity-period[commafeed.feed-refresh.user-inactivity-period]`
[.description]
--
Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
a| [[commafeed_commafeed-feed-refresh-filtering-expression-evaluation-timeout]]`link:#commafeed_commafeed-feed-refresh-filtering-expression-evaluation-timeout[commafeed.feed-refresh.filtering-expression-evaluation-timeout]`
[.description]
--
Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`500MS`
a| [[commafeed_commafeed-database-query-timeout]]`link:#commafeed_commafeed-database-query-timeout[commafeed.database.query-timeout]`
[.description]
--
Database query timeout. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
a| [[commafeed_commafeed-database-cleanup-entries-max-age]]`link:#commafeed_commafeed-database-cleanup-entries-max-age[commafeed.database.cleanup.entries-max-age]`
[.description]
--
Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`365D`
a| [[commafeed_commafeed-database-cleanup-statuses-max-age]]`link:#commafeed_commafeed-database-cleanup-statuses-max-age[commafeed.database.cleanup.statuses-max-age]`
[.description]
--
Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
a| [[commafeed_commafeed-database-cleanup-max-feed-capacity]]`link:#commafeed_commafeed-database-cleanup-max-feed-capacity[commafeed.database.cleanup.max-feed-capacity]`
[.description]
--
Maximum number of entries per feed to keep in the database. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++`
endif::add-copy-button-to-env-var[]
--|int
|`500`
a| [[commafeed_commafeed-database-cleanup-max-feeds-per-user]]`link:#commafeed_commafeed-database-cleanup-max-feeds-per-user[commafeed.database.cleanup.max-feeds-per-user]`
[.description]
--
Limit the number of feeds a user can subscribe to. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++`
endif::add-copy-button-to-env-var[]
--|int
|`0`
a| [[commafeed_commafeed-database-cleanup-batch-size]]`link:#commafeed_commafeed-database-cleanup-batch-size[commafeed.database.cleanup.batch-size]`
[.description]
--
Rows to delete per query while cleaning up old entries.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++`
endif::add-copy-button-to-env-var[]
--|@jakarta.validation.constraints.Positive int
|`100`
a| [[commafeed_commafeed-users-allow-registrations]]`link:#commafeed_commafeed-users-allow-registrations[commafeed.users.allow-registrations]`
[.description]
--
Whether to let users create accounts for themselves.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`
a| [[commafeed_commafeed-users-strict-password-policy]]`link:#commafeed_commafeed-users-strict-password-policy[commafeed.users.strict-password-policy]`
[.description]
--
Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`true`
a| [[commafeed_commafeed-users-create-demo-account]]`link:#commafeed_commafeed-users-create-demo-account[commafeed.users.create-demo-account]`
[.description]
--
Whether to create a demo account the first time the app starts.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`
a| [[commafeed_commafeed-websocket-enabled]]`link:#commafeed_commafeed-websocket-enabled[commafeed.websocket.enabled]`
[.description]
--
Enable websocket connection so the server can notify web clients that there are new entries for feeds.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_ENABLED+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`true`
a| [[commafeed_commafeed-websocket-ping-interval]]`link:#commafeed_commafeed-websocket-ping-interval[commafeed.websocket.ping-interval]`
[.description]
--
Interval at which the client will send a ping message on the websocket to keep the connection alive.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`15M`
a| [[commafeed_commafeed-websocket-tree-reload-interval]]`link:#commafeed_commafeed-websocket-tree-reload-interval[commafeed.websocket.tree-reload-interval]`
[.description]
--
If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`30S`
|===
ifndef::no-duration-note[]
[NOTE]
[id='duration-note-anchor-{summaryTableId}']
.About the Duration format
====
To write duration values, use the standard `java.time.Duration` format.
See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information.
You can also use a simplified format, starting with a number:
* If the value is only a number, it represents time in seconds.
* If the value is a number followed by `ms`, it represents time in milliseconds.
In other cases, the simplified format is translated to the `java.time.Duration` format for parsing:
* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`.
* If the value is a number followed by `d`, it is prefixed with `P`.
====
endif::no-duration-note[]
[NOTE]
[[memory-size-note-anchor]]
.About the MemorySize format
====
A size configuration option recognises string in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.
If no suffix is given, assume bytes.
====

View File

@@ -4,7 +4,7 @@ services:
mysql:
image: mariadb
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_ROOT_PASSWORD=commafeed
- MYSQL_DATABASE=commafeed
ports:
- "3306:3306"
@@ -12,8 +12,8 @@ services:
postgresql:
image: postgres
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
- POSTGRES_USER=commafeed
- POSTGRES_PASSWORD=commafeed
- POSTGRES_DB=commafeed
ports:
- "5432:5432"

View File

@@ -6,33 +6,26 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>4.5.0</version>
<version>5.0.2</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<properties>
<guice.version>7.0.0</guice.version>
<querydsl.version>6.5</querydsl.version>
<quarkus.version>3.13.2</quarkus.version>
<querydsl.version>6.6</querydsl.version>
<rome.version>2.1.0</rome.version>
<properties-plugin.version>1.2.1</properties-plugin.version>
<testcontainers.version>1.19.8</testcontainers.version>
<!-- renovate: datasource=docker depName=postgres -->
<postgresql.image.version>16.3</postgresql.image.version>
<!-- renovate: datasource=docker depName=mysql -->
<mysql.image.version>9.0.0</mysql.image.version>
<!-- renovate: datasource=docker depName=mariadb -->
<mariadb.image.version>11.4.2</mariadb.image.version>
<!-- renovate: datasource=docker depName=redis -->
<redis.image.version>7.2.5</redis.image.version>
<build.database>h2</build.database>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-dependencies</artifactId>
<version>4.0.7</version>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -40,31 +33,85 @@
</dependencyManagement>
<build>
<finalName>commafeed</finalName>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<includes>
<include>docker-images.properties</include>
</includes>
<filtering>true</filtering>
</testResource>
</testResources>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<artifactId>maven-help-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>active-profiles</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
<configuration>
<properties>
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
<quarkus.package.runner-suffix>
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
</quarkus.package.runner-suffix>
</properties>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>commafeed-${project.version}-${build.database}-jvm</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/zip-quarkus-app.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.3.0</version>
<version>3.4.0</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.3.0</version>
<version>3.4.0</version>
<executions>
<execution>
<goals>
@@ -73,6 +120,13 @@
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>io.github.git-commit-id</groupId>
@@ -93,63 +147,13 @@
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>org.kordamp.shade</groupId>
<artifactId>maven-shade-ext-transformers</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>module-info.class</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
</transformer>
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
<paths>
<path>rome.properties</path>
</paths>
<mergeStrategy>append</mergeStrategy>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin-jakarta</artifactId>
<version>2.2.22</version>
<?m2e ignore?>
<configuration>
<outputPath>${project.build.directory}/classes/assets</outputPath>
<outputPath>${project.build.directory}/classes/META-INF/resources</outputPath>
<outputFormat>JSONANDYAML</outputFormat>
<resourcePackages>
<package>com.commafeed.frontend.resource</package>
@@ -167,18 +171,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -236,7 +228,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>4.5.0</version>
<version>5.0.2</version>
</dependency>
<dependency>
@@ -246,60 +238,74 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<version>1.11</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guice.version}</version>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-liquibase</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mysql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mariadb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-unix-socket</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-hibernate</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-assets</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-forms</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
</dependency>
<dependency>
<groupId>io.whitfin</groupId>
<artifactId>dropwizard-environment-substitutor</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jakarta-server</artifactId>
<version>4.2.27</version>
</dependency>
<dependency>
@@ -321,6 +327,11 @@
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.0-jre</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
@@ -348,17 +359,6 @@
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
@@ -383,7 +383,7 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
@@ -408,7 +408,15 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
<!-- add brotli support for httpclient5 -->
<dependency>
<groupId>org.brotli</groupId>
<artifactId>dec</artifactId>
<version>0.1.2</version>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
@@ -416,40 +424,8 @@
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-youtube</artifactId>
<version>v3-rev20240514-2.0.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.manticore-projects.tools</groupId>
<artifactId>h2migrationtool</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.0.0</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -469,14 +445,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail-junit5</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -487,33 +457,153 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.45.0</version>
<version>1.46.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mariadb</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
<profile>
<id>h2</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<build.database>h2</build.database>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>${properties-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>set-system-properties</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<property>
<name>quarkus.datasource.db-kind</name>
<value>h2</value>
</property>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>mysql</id>
<properties>
<build.database>mysql</build.database>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>${properties-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>set-system-properties</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<property>
<name>quarkus.datasource.db-kind</name>
<value>mysql</value>
</property>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>mariadb</id>
<properties>
<build.database>mariadb</build.database>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>${properties-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>set-system-properties</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<property>
<name>quarkus.datasource.db-kind</name>
<value>mariadb</value>
</property>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>postgresql</id>
<properties>
<build.database>postgresql</build.database>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>${properties-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>set-system-properties</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<property>
<name>quarkus.datasource.db-kind</name>
<value>postgresql</value>
</property>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,21 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>zip-quarkus-app</id>
<includeBaseDirectory>true</includeBaseDirectory>
<baseDirectory>commafeed-${project.version}-${build.database}</baseDirectory>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.build.directory}/quarkus-app</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,15 @@
FROM ibm-semeru-runtimes:open-21.0.4_7-jre
EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
COPY commafeed-server/target/quarkus-app/ /commafeed
WORKDIR /commafeed
CMD ["java", \
"-Xtune:virtualized", \
"-Xminf0.05", \
"-Xmaxf0.1", \
"-jar", \
"quarkus-run.jar"]

View File

@@ -0,0 +1,10 @@
FROM debian:12.6
EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
COPY commafeed-server/target/commafeed-*-runner /commafeed/application
WORKDIR /commafeed
CMD ["./application"]

View File

@@ -0,0 +1,85 @@
# CommaFeed
Official docker images for https://github.com/Athou/commafeed/
## Quickstart
Start CommaFeed with a H2 embedded database. Then login as `admin/admin` on http://localhost:8082/
### docker
`docker run --name commafeed --detach --publish 8082:8082 --restart unless-stopped --volume /path/to/commafeed/db:/commafeed/data --memory 256M athou/commafeed:latest-h2`
### docker-compose
```
services:
commafeed:
image: athou/commafeed:latest-h2
restart: unless-stopped
volumes:
- /path/to/commafeed/db:/commafeed/data
deploy:
resources:
limits:
memory: 256M
ports:
- 8082:8082
```
## Advanced
While using the H2 embedded database is perfectly fine for small instances, you may want to have more control over the
database. Here's an example that uses postgresql (note image tag change from `latest-h2` to `latest-postgresql`):
```
services:
commafeed:
image: athou/commafeed:latest-postgresql
restart: unless-stopped
environment:
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql:5432/commafeed
- QUARKUS_DATASOURCE_USERNAME=commafeed
- QUARKUS_DATASOURCE_PASSWORD=commafeed
deploy:
resources:
limits:
memory: 256M
ports:
- 8082:8082
postgresql:
image: postgres:latest
restart: unless-stopped
environment:
POSTGRES_USER: commafeed
POSTGRES_PASSWORD: commafeed
POSTGRES_DB: commafeed
volumes:
- /path/to/commafeed/db:/var/lib/postgresql/data
```
## Configuration
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/doc/commafeed.adoc) are
optional and have sensible default values.
Settings are overrideable with environment variables. For instance, `commafeed.feed-refresh.interval-empirical` can be
set with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL` variable.
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
`QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` variable to a fixed value (min. 16 characters).
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
## Docker tags
Tags are of the form `<version>-<database>[-jvm]` where:
- `<version>` is either:
- a specific CommaFeed version (e.g. `5.0.0`)
- `latest` (always points to the latest version)
- `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. This image supports
the arm64 platform which is not yet supported by the native image.

View File

@@ -1,277 +1,40 @@
package com.commafeed;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
import org.hibernate.cfg.AvailableSettings;
import com.codahale.metrics.json.MetricsModule;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.feed.FeedRefreshEngine;
import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
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.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.UserService;
import com.commafeed.backend.service.db.DatabaseStartupService;
import com.commafeed.backend.service.db.H2MigrationService;
import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.frontend.auth.PasswordConstraintValidator;
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST;
import com.commafeed.frontend.resource.EntryREST;
import com.commafeed.frontend.resource.FeedREST;
import com.commafeed.frontend.resource.ServerREST;
import com.commafeed.frontend.resource.UserREST;
import com.commafeed.frontend.resource.fever.FeverREST;
import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet;
import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.commafeed.frontend.ws.WebSocketConfigurator;
import com.commafeed.frontend.ws.WebSocketEndpoint;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.commafeed.backend.task.TaskScheduler;
import com.commafeed.security.password.PasswordConstraintValidator;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
import io.dropwizard.configuration.SubstitutingSourceProvider;
import io.dropwizard.core.Application;
import io.dropwizard.core.ConfiguredBundle;
import io.dropwizard.core.setup.Bootstrap;
import io.dropwizard.core.setup.Environment;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.forms.MultiPartBundle;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter;
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.websocket.server.ServerEndpointConfig;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@Singleton
@RequiredArgsConstructor
public class CommaFeedApplication {
public static final String USERNAME_ADMIN = "admin";
public static final String USERNAME_DEMO = "demo";
public static final Instant STARTUP_TIME = Instant.now();
private final DatabaseStartupService databaseStartupService;
private final FeedRefreshEngine feedRefreshEngine;
private final TaskScheduler taskScheduler;
private final CommaFeedConfiguration config;
private HibernateBundle<CommaFeedConfiguration> hibernateBundle;
public void start(@Observes StartupEvent ev) {
PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy());
@Override
public String getName() {
return "CommaFeed";
databaseStartupService.populateInitialData();
feedRefreshEngine.start();
taskScheduler.start();
}
@Override
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
configureEnvironmentSubstitutor(bootstrap);
configureObjectMapper(bootstrap.getObjectMapper());
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
bootstrap.addBundle(new ConfiguredBundle<>() {
@Override
public void run(CommaFeedConfiguration config, Environment environment) {
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
String url = dataSourceFactory.getUrl();
if (isFileBasedH2(url)) {
Path path = getFilePath(url);
String user = dataSourceFactory.getUser();
String password = dataSourceFactory.getPassword();
new H2MigrationService().migrateIfNeeded(path, user, password);
}
}
private boolean isFileBasedH2(String url) {
return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:");
}
private Path getFilePath(String url) {
String name = url.substring("jdbc:h2:".length()).split(";")[0];
return Paths.get(name + ".mv.db");
}
});
bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class,
FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class,
UserSettings.class) {
@Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
DataSourceFactory factory = configuration.getDataSourceFactory();
factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
return factory;
}
});
bootstrap.addBundle(new MigrationsBundle<>() {
@Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
bootstrap.addBundle(new MultiPartBundle());
public void stop(@Observes ShutdownEvent ev) {
feedRefreshEngine.stop();
taskScheduler.stop();
}
private static void configureEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() {
@Override
protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
// disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty
return objectMapper
.setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
}
});
bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap));
}
private static void configureObjectMapper(ObjectMapper objectMapper) {
// read and write instants as milliseconds instead of nanoseconds
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
// add support for serializing metrics
objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
}
private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
// enable config.yml string substitution
// e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR
SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
new EnvironmentVariableSubstitutor(false));
// enable config.yml properties override with env variables prefixed with CF_
// e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true
return new EnvironmentSubstitutor("CF", substitutingSourceProvider);
}
@Override
public void run(CommaFeedConfiguration config, Environment environment) {
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
// guice init
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
// session management
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
// support for "@SecurityCheck User user" injection
environment.jersey()
.register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class),
injector.getInstance(UserService.class), config));
// support for "@Context SessionHelper sessionHelper" injection
environment.jersey().register(new SessionHelperFactoryProvider.Binder());
// REST resources
environment.jersey().setUrlPattern("/rest/*");
environment.jersey().register(injector.getInstance(AdminREST.class));
environment.jersey().register(injector.getInstance(CategoryREST.class));
environment.jersey().register(injector.getInstance(EntryREST.class));
environment.jersey().register(injector.getInstance(FeedREST.class));
environment.jersey().register(injector.getInstance(ServerREST.class));
environment.jersey().register(injector.getInstance(UserREST.class));
environment.jersey().register(injector.getInstance(FeverREST.class));
// Servlets
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");
environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout");
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
environment.servlets()
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
.addMapping("/robots.txt");
}
// WebSocket endpoint
JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000);
container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
.configurator(injector.getInstance(WebSocketConfigurator.class))
.build());
});
// Scheduled tasks
Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<>() {
}));
ScheduledExecutorService executor = environment.lifecycle()
.scheduledExecutorService("task-scheduler", true)
.threads(tasks.size())
.build();
for (ScheduledTask task : tasks) {
task.register(executor);
}
// database init/changelogs
environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
// start feed fetching engine
environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
// prevent caching index.html, so that the webapp is always up to date
environment.servlets()
.addFilter("index-cache-busting-filter", new CacheBustingFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/");
// prevent caching openapi files, so that the documentation is always up to date
environment.servlets()
.addFilter("openapi-cache-busting-filter", new CacheBustingFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml");
// prevent caching REST resources, except for favicons
environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String path = ((HttpServletRequest) request).getRequestURI();
if (path.contains("/feed/favicon")) {
chain.doFilter(request, response);
} else {
super.doFilter(request, response, chain);
}
}
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
}
public static void main(String[] args) throws Exception {
new CommaFeedApplication().run(args);
}
}

View File

@@ -1,183 +1,281 @@
package com.commafeed;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Properties;
import java.util.Optional;
import com.commafeed.backend.cache.RedisPoolFactory;
import com.commafeed.frontend.session.SessionHandlerFactory;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
import io.dropwizard.core.Configuration;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.util.Duration;
import jakarta.validation.Valid;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.configuration.MemorySize;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CommaFeedConfiguration extends Configuration {
/**
* CommaFeed configuration
*
* Default values are for production, they can be overridden in application.properties for other profiles
*/
@ConfigMapping(prefix = "commafeed")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface CommaFeedConfiguration {
/**
* Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
*/
@WithDefault("true")
boolean hideFromWebCrawlers();
public enum CacheType {
NOOP, REDIS
/**
* If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser.
*
* This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
*/
@WithDefault("false")
boolean imageProxyEnabled();
/**
* Enable password recovery via email.
*
* Quarkus mailer will need to be configured.
*/
@WithDefault("false")
boolean passwordRecoveryEnabled();
/**
* Message displayed in a notification at the bottom of the page.
*/
Optional<String> announcement();
/**
* Google Analytics tracking code.
*/
Optional<String> googleAnalyticsTrackingCode();
/**
* Google Auth key for fetching Youtube channel favicons.
*/
Optional<String> googleAuthKey();
/**
* HTTP client configuration
*/
HttpClient httpClient();
/**
* Feed refresh engine settings.
*/
FeedRefresh feedRefresh();
/**
* Database settings.
*/
Database database();
/**
* Users settings.
*/
Users users();
/**
* Websocket settings.
*/
Websocket websocket();
interface HttpClient {
/**
* User-Agent string that will be used by the http client, leave empty for the default one.
*/
Optional<String> userAgent();
/**
* Time to wait for a connection to be established.
*/
@WithDefault("5s")
Duration connectTimeout();
/**
* Time to wait for SSL handshake to complete.
*/
@WithDefault("5s")
Duration sslHandshakeTimeout();
/**
* Time to wait between two packets before timeout.
*/
@WithDefault("10s")
Duration socketTimeout();
/**
* Time to wait for the full response to be received.
*/
@WithDefault("10s")
Duration responseTimeout();
/**
* Time to live for a connection in the pool.
*/
@WithDefault("30s")
Duration connectionTimeToLive();
/**
* Time between eviction runs for idle connections.
*/
@WithDefault("1m")
Duration idleConnectionsEvictionInterval();
/**
* If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
*/
@WithDefault("5M")
MemorySize maxResponseSize();
}
@Valid
@NotNull
@JsonProperty("database")
private final DataSourceFactory dataSourceFactory = new DataSourceFactory();
interface FeedRefresh {
/**
* Amount of time CommaFeed will wait before refreshing the same feed.
*/
@WithDefault("5m")
Duration interval();
@Valid
@NotNull
@JsonProperty("redis")
private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory();
/**
* If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the
* last entry was published. The interval will be somewhere between the default refresh interval and 24h.
*
* See {@link FeedRefreshIntervalCalculator} for details.
*/
@WithDefault("false")
boolean intervalEmpirical();
@Valid
@NotNull
@JsonProperty("session")
private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory();
/**
* Amount of http threads used to fetch feeds.
*/
@Min(1)
@WithDefault("3")
int httpThreads();
@Valid
@NotNull
@JsonProperty("app")
private ApplicationSettings applicationSettings;
/**
* Amount of threads used to insert new entries in the database.
*/
@Min(1)
@WithDefault("1")
int databaseThreads();
private final String version;
private final String gitCommit;
/**
* Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
*
* 0 to disable.
*/
@WithDefault("0")
Duration userInactivityPeriod();
public CommaFeedConfiguration() {
Properties properties = new Properties();
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
if (stream != null) {
properties.load(stream);
/**
* Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
*/
@WithDefault("500ms")
Duration filteringExpressionEvaluationTimeout();
}
interface Database {
/**
* Database query timeout.
*
* 0 to disable.
*/
@WithDefault("0")
Duration queryTimeout();
/**
* Database cleanup settings.
*/
Cleanup cleanup();
interface Cleanup {
/**
* Maximum age of feed entries in the database. Older entries will be deleted.
*
* 0 to disable.
*/
@WithDefault("365d")
Duration entriesMaxAge();
/**
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
*
* 0 to disable.
*/
@WithDefault("0")
Duration statusesMaxAge();
/**
* Maximum number of entries per feed to keep in the database.
*
* 0 to disable.
*/
@WithDefault("500")
int maxFeedCapacity();
/**
* Limit the number of feeds a user can subscribe to.
*
* 0 to disable.
*/
@WithDefault("0")
int maxFeedsPerUser();
/**
* Rows to delete per query while cleaning up old entries.
*/
@Positive
@WithDefault("100")
int batchSize();
default Instant statusesInstantThreshold() {
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
this.version = properties.getProperty("git.build.version", "unknown");
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
}
@Getter
@Setter
public static class ApplicationSettings {
@NotNull
@NotBlank
@Valid
private String publicUrl;
interface Users {
/**
* Whether to let users create accounts for themselves.
*/
@WithDefault("false")
boolean allowRegistrations();
@NotNull
@Valid
private Boolean hideFromWebCrawlers = true;
/**
* Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
*/
@WithDefault("true")
boolean strictPasswordPolicy();
@NotNull
@Valid
private Boolean allowRegistrations;
/**
* Whether to create a demo account the first time the app starts.
*/
@WithDefault("false")
boolean createDemoAccount();
}
@NotNull
@Valid
private Boolean strictPasswordPolicy = true;
interface Websocket {
/**
* Enable websocket connection so the server can notify web clients that there are new entries for feeds.
*/
@WithDefault("true")
boolean enabled();
@NotNull
@Valid
private Boolean createDemoAccount;
private String googleAnalyticsTrackingCode;
private String googleAuthKey;
@NotNull
@Min(1)
@Valid
private Integer backgroundThreads;
@NotNull
@Min(1)
@Valid
private Integer databaseUpdateThreads;
@NotNull
@Positive
@Valid
private Integer databaseCleanupBatchSize = 100;
private String smtpHost;
private int smtpPort;
private boolean smtpTls;
private String smtpUserName;
private String smtpPassword;
private String smtpFromAddress;
private boolean graphiteEnabled;
private String graphitePrefix;
private String graphiteHost;
private int graphitePort;
private int graphiteInterval;
@NotNull
@Valid
private Boolean heavyLoad;
@NotNull
@Valid
private Boolean imageProxyEnabled;
@NotNull
@Min(0)
@Valid
private Integer queryTimeout;
@NotNull
@Min(0)
@Valid
private Integer keepStatusDays;
@NotNull
@Min(0)
@Valid
private Integer maxFeedCapacity;
@NotNull
@Min(0)
@Valid
private Integer maxEntriesAgeDays = 0;
@NotNull
@Valid
private Integer maxFeedsPerUser = 0;
@NotNull
@Min(0)
@Valid
private Integer refreshIntervalMinutes;
@NotNull
@Valid
private CacheType cache;
@Valid
private String announcement;
private String userAgent;
private Boolean websocketEnabled = true;
private Duration websocketPingInterval = Duration.minutes(15);
private Duration treeReloadInterval = Duration.seconds(30);
public Instant getUnreadThreshold() {
return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null;
}
/**
* Interval at which the client will send a ping message on the websocket to keep the connection alive.
*/
@WithDefault("15m")
Duration pingInterval();
/**
* If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
*/
@WithDefault("30s")
Duration treeReloadInterval();
}
}

View File

@@ -1,97 +0,0 @@
package com.commafeed;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import org.hibernate.SessionFactory;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.graphite.Graphite;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
import com.commafeed.CommaFeedConfiguration.CacheType;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.cache.NoopCacheService;
import com.commafeed.backend.cache.RedisCacheService;
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
import com.commafeed.backend.favicon.FacebookFaviconFetcher;
import com.commafeed.backend.favicon.YoutubeFaviconFetcher;
import com.commafeed.backend.task.DemoAccountCleanupTask;
import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask;
import com.commafeed.backend.task.OldEntriesCleanupTask;
import com.commafeed.backend.task.OldStatusesCleanupTask;
import com.commafeed.backend.task.OrphanedContentsCleanupTask;
import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider;
import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.multibindings.Multibinder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class CommaFeedModule extends AbstractModule {
@Getter(onMethod = @__({ @Provides }))
private final SessionFactory sessionFactory;
@Getter(onMethod = @__({ @Provides }))
private final CommaFeedConfiguration config;
@Getter(onMethod = @__({ @Provides }))
private final MetricRegistry metrics;
@Override
protected void configure() {
CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService()
: new RedisCacheService(config.getRedisPoolFactory().build());
log.info("using cache {}", cacheService.getClass());
bind(CacheService.class).toInstance(cacheService);
Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
Multibinder<FeedURLProvider> urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class);
urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class);
urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class);
Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class);
taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
taskMultibinder.addBinding().to(DemoAccountCleanupTask.class);
ApplicationSettings settings = config.getApplicationSettings();
if (settings.isGraphiteEnabled()) {
final String graphitePrefix = settings.getGraphitePrefix();
final String graphiteHost = settings.getGraphiteHost();
final int graphitePort = settings.getGraphitePort();
final int graphiteInterval = settings.getGraphiteInterval();
log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort,
graphitePrefix, graphiteInterval);
final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics)
.prefixedWith(graphitePrefix)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.filter(MetricFilter.ALL)
.build(graphite);
reporter.start(graphiteInterval, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,16 @@
package com.commafeed;
import com.codahale.metrics.MetricRegistry;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
@Singleton
public class CommaFeedProducers {
@Produces
@Singleton
public MetricRegistry metricRegistry() {
return new MetricRegistry();
}
}

View File

@@ -0,0 +1,31 @@
package com.commafeed;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import jakarta.inject.Singleton;
import lombok.Getter;
@Singleton
@Getter
public class CommaFeedVersion {
private final String version;
private final String gitCommit;
public CommaFeedVersion() {
Properties properties = new Properties();
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
if (stream != null) {
properties.load(stream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
this.version = properties.getProperty("git.build.version", "unknown");
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
}
}

View File

@@ -0,0 +1,49 @@
package com.commafeed;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.RestResponse.Status;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.UnauthorizedException;
import jakarta.annotation.Priority;
import jakarta.validation.ValidationException;
import jakarta.ws.rs.ext.Provider;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Provider
@Priority(1)
public class ExceptionMappers {
private final CommaFeedConfiguration config;
@ServerExceptionMapper(UnauthorizedException.class)
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
return RestResponse.status(RestResponse.Status.UNAUTHORIZED,
new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
}
@ServerExceptionMapper(AuthenticationFailedException.class)
public RestResponse<AuthenticationFailed> authenticationFailed(AuthenticationFailedException e) {
return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage()));
}
@ServerExceptionMapper(ValidationException.class)
public RestResponse<ValidationFailed> validationFailed(ValidationException e) {
return RestResponse.status(Status.BAD_REQUEST, new ValidationFailed(e.getMessage()));
}
@RegisterForReflection
public record UnauthorizedResponse(String message, boolean allowRegistrations) {
}
@RegisterForReflection
public record AuthenticationFailed(String message) {
}
@RegisterForReflection
public record ValidationFailed(String message) {
}
}

View File

@@ -0,0 +1,27 @@
package com.commafeed;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.json.MetricsModule;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
@Singleton
public class JacksonCustomizer implements ObjectMapperCustomizer {
@Override
public void customize(ObjectMapper objectMapper) {
objectMapper.registerModule(new JavaTimeModule());
// read and write instants as milliseconds instead of nanoseconds
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
// add support for serializing metrics
objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
}
}

View File

@@ -0,0 +1,226 @@
package com.commafeed;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection(
targets = {
// metrics
MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class,
// rome
java.util.Date.class, com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class,
com.rometools.rome.feed.synd.SyndFeedImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class,
com.rometools.rome.feed.synd.SyndEntryImpl.class, com.rometools.modules.psc.types.SimpleChapter.class,
com.rometools.rome.feed.synd.SyndCategoryImpl.class, com.rometools.rome.feed.synd.SyndImageImpl.class,
com.rometools.rome.feed.synd.SyndContentImpl.class, com.rometools.rome.feed.synd.SyndEnclosureImpl.class,
// rome cloneable
com.rometools.modules.activitystreams.types.Article.class, com.rometools.modules.activitystreams.types.Audio.class,
com.rometools.modules.activitystreams.types.Bookmark.class, com.rometools.modules.activitystreams.types.Comment.class,
com.rometools.modules.activitystreams.types.Event.class, com.rometools.modules.activitystreams.types.File.class,
com.rometools.modules.activitystreams.types.Folder.class, com.rometools.modules.activitystreams.types.List.class,
com.rometools.modules.activitystreams.types.Note.class, com.rometools.modules.activitystreams.types.Person.class,
com.rometools.modules.activitystreams.types.Photo.class, com.rometools.modules.activitystreams.types.PhotoAlbum.class,
com.rometools.modules.activitystreams.types.Place.class, com.rometools.modules.activitystreams.types.Playlist.class,
com.rometools.modules.activitystreams.types.Product.class, com.rometools.modules.activitystreams.types.Review.class,
com.rometools.modules.activitystreams.types.Service.class, com.rometools.modules.activitystreams.types.Song.class,
com.rometools.modules.activitystreams.types.Status.class, com.rometools.modules.base.types.DateTimeRange.class,
com.rometools.modules.base.types.FloatUnit.class, com.rometools.modules.base.types.GenderEnumeration.class,
com.rometools.modules.base.types.IntUnit.class, com.rometools.modules.base.types.PriceTypeEnumeration.class,
com.rometools.modules.base.types.ShippingType.class, com.rometools.modules.base.types.ShortDate.class,
com.rometools.modules.base.types.Size.class, com.rometools.modules.base.types.YearType.class,
com.rometools.modules.content.ContentItem.class, com.rometools.modules.georss.GeoRSSPoint.class,
com.rometools.modules.georss.geometries.Envelope.class, com.rometools.modules.georss.geometries.LineString.class,
com.rometools.modules.georss.geometries.LinearRing.class, com.rometools.modules.georss.geometries.Point.class,
com.rometools.modules.georss.geometries.Polygon.class, com.rometools.modules.georss.geometries.Position.class,
com.rometools.modules.georss.geometries.PositionList.class, com.rometools.modules.mediarss.types.MediaGroup.class,
com.rometools.modules.mediarss.types.Metadata.class, com.rometools.modules.mediarss.types.Thumbnail.class,
com.rometools.modules.opensearch.entity.OSQuery.class, com.rometools.modules.photocast.types.PhotoDate.class,
com.rometools.modules.sle.types.DateValue.class, com.rometools.modules.sle.types.Group.class,
com.rometools.modules.sle.types.NumberValue.class, com.rometools.modules.sle.types.Sort.class,
com.rometools.modules.sle.types.StringValue.class, com.rometools.modules.yahooweather.types.Astronomy.class,
com.rometools.modules.yahooweather.types.Atmosphere.class, com.rometools.modules.yahooweather.types.Condition.class,
com.rometools.modules.yahooweather.types.Forecast.class, com.rometools.modules.yahooweather.types.Location.class,
com.rometools.modules.yahooweather.types.Units.class, com.rometools.modules.yahooweather.types.Wind.class,
com.rometools.opml.feed.opml.Attribute.class, com.rometools.opml.feed.opml.Opml.class,
com.rometools.opml.feed.opml.Outline.class, com.rometools.rome.feed.atom.Category.class,
com.rometools.rome.feed.atom.Content.class, com.rometools.rome.feed.atom.Entry.class,
com.rometools.rome.feed.atom.Feed.class, com.rometools.rome.feed.atom.Generator.class,
com.rometools.rome.feed.atom.Link.class, com.rometools.rome.feed.atom.Person.class,
com.rometools.rome.feed.rss.Category.class, com.rometools.rome.feed.rss.Channel.class,
com.rometools.rome.feed.rss.Cloud.class, com.rometools.rome.feed.rss.Content.class,
com.rometools.rome.feed.rss.Description.class, com.rometools.rome.feed.rss.Enclosure.class,
com.rometools.rome.feed.rss.Guid.class, com.rometools.rome.feed.rss.Image.class, com.rometools.rome.feed.rss.Item.class,
com.rometools.rome.feed.rss.Source.class, com.rometools.rome.feed.rss.TextInput.class,
com.rometools.rome.feed.synd.SyndLinkImpl.class, com.rometools.rome.feed.synd.SyndPersonImpl.class,
java.util.ArrayList.class,
// rome modules
com.rometools.modules.sse.modules.Conflict.class, com.rometools.modules.sse.modules.Conflicts.class,
com.rometools.modules.cc.CreativeCommonsImpl.class, com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class, com.rometools.modules.sse.modules.Sharing.class,
com.rometools.modules.georss.SimpleModuleImpl.class, com.rometools.modules.atom.modules.AtomLinkModuleImpl.class,
com.rometools.modules.itunes.EntryInformationImpl.class, com.rometools.modules.sse.modules.Update.class,
com.rometools.modules.photocast.PhotocastModuleImpl.class, com.rometools.modules.itunes.FeedInformationImpl.class,
com.rometools.modules.yahooweather.YWeatherModuleImpl.class, com.rometools.modules.feedburner.FeedBurnerImpl.class,
com.rometools.modules.sse.modules.Related.class, com.rometools.modules.fyyd.modules.FyydModuleImpl.class,
com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class, com.rometools.modules.thr.ThreadingModuleImpl.class,
com.rometools.modules.sse.modules.Sync.class, com.rometools.modules.sle.SimpleListExtensionImpl.class,
com.rometools.modules.slash.SlashImpl.class, com.rometools.modules.sse.modules.History.class,
com.rometools.modules.georss.GMLModuleImpl.class, com.rometools.modules.base.CustomTagsImpl.class,
com.rometools.modules.base.GoogleBaseImpl.class, com.rometools.modules.sle.SleEntryImpl.class,
com.rometools.modules.mediarss.MediaEntryModuleImpl.class, com.rometools.modules.content.ContentModuleImpl.class,
com.rometools.modules.georss.W3CGeoModuleImpl.class, com.rometools.rome.feed.module.DCModuleImpl.class,
com.rometools.modules.mediarss.MediaModuleImpl.class, com.rometools.rome.feed.module.SyModuleImpl.class,
// extracted from all 3 rome.properties files of rome library
com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class,
com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class,
com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class,
com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class,
com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class,
com.rometools.rome.io.impl.Atom03Parser.class,
com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class,
com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class,
com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class,
com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class,
com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class,
com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
com.rometools.modules.mediarss.io.RSS20YahooParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.itunes.io.ITunesGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class,
com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class,
com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, })
public class NativeImageClasses {
}

View File

@@ -1,37 +1,41 @@
package com.commafeed.backend;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.config.TlsConfig;
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.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.eclipse.jetty.http.HttpStatus;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedVersion;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -41,27 +45,36 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
/**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
*
*/
@Singleton
@Slf4j
public class HttpGetter {
private final CommaFeedConfiguration config;
private final CloseableHttpClient client;
@Inject
public HttpGetter(CommaFeedConfiguration config) {
String userAgent = Optional.ofNullable(config.getApplicationSettings().getUserAgent())
.orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion()));
this.client = newClient(userAgent, config.getApplicationSettings().getBackgroundThreads());
public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) {
this.config = config;
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
String userAgent = config.httpClient()
.userAgent()
.orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
this.client = newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval());
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax());
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"),
() -> connectionManager.getTotalStats().getAvailable() + connectionManager.getTotalStats().getLeased());
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "leased"), () -> connectionManager.getTotalStats().getLeased());
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "pending"), () -> connectionManager.getTotalStats().getPending());
}
public HttpResult getBinary(String url, int timeout) throws IOException, NotModifiedException {
return getBinary(url, null, null, timeout);
public HttpResult getBinary(String url) throws IOException, NotModifiedException {
return getBinary(url, null, null);
}
/**
*
* @param url
* the url to retrive
* @param lastModified
@@ -71,10 +84,9 @@ public class HttpGetter {
* @throws NotModifiedException
* if the url hasn't changed since we asked for it last time
*/
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
public HttpResult getBinary(String url, String lastModified, String eTag) throws IOException, NotModifiedException {
log.debug("fetching {}", url);
long start = System.currentTimeMillis();
ClassicHttpRequest request = ClassicRequestBuilder.get(url).build();
if (lastModified != null) {
request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
@@ -84,9 +96,10 @@ public class HttpGetter {
}
HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build());
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
HttpResponse response = client.execute(request, 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)
@@ -97,7 +110,6 @@ public class HttpGetter {
.map(StringUtils::trimToNull)
.orElse(null);
byte[] content = resp.getEntity() == null ? null : EntityUtils.toByteArray(resp.getEntity());
String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null);
String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
.map(RedirectLocations::getAll)
@@ -109,7 +121,7 @@ public class HttpGetter {
});
int code = response.getCode();
if (code == HttpStatus.NOT_MODIFIED_304) {
if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("'304 - not modified' http code received");
} else if (code >= 300) {
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
@@ -125,27 +137,54 @@ public class HttpGetter {
throw new NotModifiedException("eTagHeader is the same");
}
long duration = System.currentTimeMillis() - start;
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader, duration,
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader,
response.getUrlAfterRedirect());
}
private static CloseableHttpClient newClient(String userAgent, int poolSize) {
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).readAllBytes();
if (bytes.length == maxBytes) {
throw new IOException("Response size exceeds the maximum allowed size (%s bytes)".formatted(maxBytes));
}
return bytes;
}
}
private static PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) {
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
int poolSize = config.feedRefresh().httpThreads();
return PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory))
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
.setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
.setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
.build())
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
.setMaxConnPerRoute(poolSize)
.setMaxConnTotal(poolSize)
.build();
}
private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent,
Duration idleConnectionsEvictionInterval) {
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"));
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory))
.setDefaultConnectionConfig(
ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(5)).setTimeToLive(TimeValue.ofSeconds(30)).build())
.setMaxConnPerRoute(poolSize)
.setMaxConnTotal(poolSize)
.build();
return HttpClientBuilder.create()
.useSystemProperties()
.disableAutomaticRetries()
@@ -153,6 +192,8 @@ public class HttpGetter {
.setUserAgent(userAgent)
.setDefaultHeaders(headers)
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval))
.build();
}
@@ -212,7 +253,6 @@ public class HttpGetter {
private final String contentType;
private final String lastModifiedSince;
private final String eTag;
private final long duration;
private final String urlAfterRedirect;
}

View File

@@ -1,39 +0,0 @@
package com.commafeed.backend.cache;
import java.util.List;
import java.util.Set;
import com.commafeed.backend.Digests;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount;
public abstract class CacheService {
// feed entries for faster refresh
public abstract Set<String> getLastEntries(Feed feed);
public abstract void setLastEntries(Feed feed, List<String> entries);
public String buildUniqueEntryKey(Entry entry) {
return Digests.sha1Hex(entry.guid() + entry.url());
}
// user categories
public abstract Category getUserRootCategory(User user);
public abstract void setUserRootCategory(User user, Category category);
public abstract void invalidateUserRootCategory(User... users);
// unread count
public abstract UnreadCount getUnreadCount(FeedSubscription sub);
public abstract void setUnreadCount(FeedSubscription sub, UnreadCount count);
public abstract void invalidateUnreadCount(FeedSubscription... subs);
}

View File

@@ -1,54 +0,0 @@
package com.commafeed.backend.cache;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount;
public class NoopCacheService extends CacheService {
@Override
public Set<String> getLastEntries(Feed feed) {
return Collections.emptySet();
}
@Override
public void setLastEntries(Feed feed, List<String> entries) {
}
@Override
public UnreadCount getUnreadCount(FeedSubscription sub) {
return null;
}
@Override
public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
}
@Override
public void invalidateUnreadCount(FeedSubscription... subs) {
}
@Override
public Category getUserRootCategory(User user) {
return null;
}
@Override
public void setUserRootCategory(User user, Category category) {
}
@Override
public void invalidateUserRootCategory(User... users) {
}
}

View File

@@ -1,154 +0,0 @@
package com.commafeed.backend.cache;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
@Slf4j
@RequiredArgsConstructor
public class RedisCacheService extends CacheService {
private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
private final JedisPool pool;
@Override
public Set<String> getLastEntries(Feed feed) {
try (Jedis jedis = pool.getResource()) {
String key = buildRedisEntryKey(feed);
return jedis.smembers(key);
}
}
@Override
public void setLastEntries(Feed feed, List<String> entries) {
try (Jedis jedis = pool.getResource()) {
String key = buildRedisEntryKey(feed);
Pipeline pipe = jedis.pipelined();
pipe.del(key);
for (String entry : entries) {
pipe.sadd(key, entry);
}
pipe.expire(key, (int) TimeUnit.DAYS.toSeconds(7));
pipe.sync();
}
}
@Override
public Category getUserRootCategory(User user) {
Category cat = null;
try (Jedis jedis = pool.getResource()) {
String key = buildRedisUserRootCategoryKey(user);
String json = jedis.get(key);
if (json != null) {
cat = MAPPER.readValue(json, Category.class);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return cat;
}
@Override
public void setUserRootCategory(User user, Category category) {
try (Jedis jedis = pool.getResource()) {
String key = buildRedisUserRootCategoryKey(user);
Pipeline pipe = jedis.pipelined();
pipe.del(key);
pipe.set(key, MAPPER.writeValueAsString(category));
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
pipe.sync();
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
}
@Override
public UnreadCount getUnreadCount(FeedSubscription sub) {
UnreadCount count = null;
try (Jedis jedis = pool.getResource()) {
String key = buildRedisUnreadCountKey(sub);
String json = jedis.get(key);
if (json != null) {
count = MAPPER.readValue(json, UnreadCount.class);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return count;
}
@Override
public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
try (Jedis jedis = pool.getResource()) {
String key = buildRedisUnreadCountKey(sub);
Pipeline pipe = jedis.pipelined();
pipe.del(key);
pipe.set(key, MAPPER.writeValueAsString(count));
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
pipe.sync();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
@Override
public void invalidateUserRootCategory(User... users) {
try (Jedis jedis = pool.getResource()) {
Pipeline pipe = jedis.pipelined();
if (users != null) {
for (User user : users) {
String key = buildRedisUserRootCategoryKey(user);
pipe.del(key);
}
}
pipe.sync();
}
}
@Override
public void invalidateUnreadCount(FeedSubscription... subs) {
try (Jedis jedis = pool.getResource()) {
Pipeline pipe = jedis.pipelined();
if (subs != null) {
for (FeedSubscription sub : subs) {
String key = buildRedisUnreadCountKey(sub);
pipe.del(key);
}
}
pipe.sync();
}
}
private String buildRedisEntryKey(Feed feed) {
return "f:" + Models.getId(feed);
}
private String buildRedisUserRootCategoryKey(User user) {
return "c:" + Models.getId(user);
}
private String buildRedisUnreadCountKey(FeedSubscription sub) {
return "u:" + Models.getId(sub);
}
}

View File

@@ -1,51 +0,0 @@
package com.commafeed.backend.cache;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
@Getter
public class RedisPoolFactory {
@JsonProperty
private String host = "localhost";
@JsonProperty
private int port = Protocol.DEFAULT_PORT;
@JsonProperty
private String username;
@JsonProperty
private String password;
@JsonProperty
private int timeout = Protocol.DEFAULT_TIMEOUT;
@JsonProperty
private int database = Protocol.DEFAULT_DATABASE;
@JsonProperty
private int maxTotal = 500;
public JedisPool build() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.user(username)
.password(password)
.timeoutMillis(timeout)
.database(database)
.build();
return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig);
}
}

View File

@@ -3,25 +3,22 @@ package com.commafeed.backend.dao;
import java.util.List;
import java.util.Objects;
import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User;
import com.querydsl.core.types.Predicate;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
@Inject
public FeedCategoryDAO(SessionFactory sessionFactory) {
super(sessionFactory);
public FeedCategoryDAO(EntityManager entityManager) {
super(entityManager, FeedCategory.class);
}
public List<FeedCategory> findAll(User user) {

View File

@@ -4,7 +4,6 @@ import java.time.Instant;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed;
@@ -12,8 +11,8 @@ import com.commafeed.backend.model.QFeedSubscription;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedDAO extends GenericDAO<Feed> {
@@ -21,9 +20,12 @@ public class FeedDAO extends GenericDAO<Feed> {
private static final QFeed FEED = QFeed.feed;
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
@Inject
public FeedDAO(SessionFactory sessionFactory) {
super(sessionFactory);
public FeedDAO(EntityManager entityManager) {
super(entityManager, Feed.class);
}
public List<Feed> findByIds(List<Long> id) {
return query().selectFrom(FEED).where(FEED.id.in(id)).fetch();
}
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {

View File

@@ -2,16 +2,12 @@ package com.commafeed.backend.dao;
import java.util.List;
import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLSubQuery;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
@@ -19,9 +15,8 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
@Inject
public FeedEntryContentDAO(SessionFactory sessionFactory) {
super(sessionFactory);
public FeedEntryContentDAO(EntityManager entityManager) {
super(entityManager, FeedEntryContent.class);
}
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
@@ -29,9 +24,13 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
}
public long deleteWithoutEntries(int max) {
JPQLSubQuery<Integer> subQuery = JPAExpressions.selectOne().from(ENTRY).where(ENTRY.content.id.eq(CONTENT.id));
List<Long> ids = query().select(CONTENT.id).from(CONTENT).where(subQuery.notExists()).limit(max).fetch();
List<Long> ids = query().select(CONTENT.id)
.from(CONTENT)
.leftJoin(ENTRY)
.on(ENTRY.content.id.eq(CONTENT.id))
.where(ENTRY.id.isNull())
.limit(max)
.fetch();
return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
}
}

View File

@@ -3,16 +3,14 @@ package com.commafeed.backend.dao;
import java.time.Instant;
import java.util.List;
import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.NumberExpression;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -21,9 +19,8 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
@Inject
public FeedEntryDAO(SessionFactory sessionFactory) {
super(sessionFactory);
public FeedEntryDAO(EntityManager entityManager) {
super(entityManager, FeedEntry.class);
}
public FeedEntry findExisting(String guidHash, Feed feed) {
@@ -50,7 +47,11 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
* Delete entries older than a certain date
*/
public int deleteEntriesOlderThan(Instant olderThan, long max) {
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.updated.lt(olderThan)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
List<FeedEntry> list = query().selectFrom(ENTRY)
.where(ENTRY.published.lt(olderThan))
.orderBy(ENTRY.published.asc())
.limit(max)
.fetch();
return delete(list);
}
@@ -58,7 +59,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
* Delete the oldest entries of a feed
*/
public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max).fetch();
return delete(list);
}

View File

@@ -7,7 +7,6 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.feed.FeedEntryKeyword;
@@ -28,8 +27,8 @@ import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
@@ -42,9 +41,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private final FeedEntryTagDAO feedEntryTagDAO;
private final CommaFeedConfiguration config;
@Inject
public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
super(sessionFactory);
public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
super(entityManager, FeedEntryStatus.class);
this.feedEntryTagDAO = feedEntryTagDAO;
this.config = config;
}
@@ -60,8 +58,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
*/
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) {
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
boolean read = unreadThreshold != null && entry.getUpdated().isBefore(unreadThreshold);
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold);
status = new FeedEntryStatus(user, sub, entry);
status.setRead(read);
status.setMarkable(!read);
@@ -84,6 +82,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
boolean includeContent) {
JPAQuery<FeedEntryStatus> query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
if (includeContent) {
query.join(STATUS.entry).fetchJoin();
query.join(STATUS.entry.content).fetchJoin();
}
@@ -92,9 +91,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
if (order == ReadingOrder.asc) {
query.orderBy(STATUS.entryUpdated.asc(), STATUS.id.asc());
query.orderBy(STATUS.entryPublished.asc(), STATUS.id.asc());
} else {
query.orderBy(STATUS.entryUpdated.desc(), STATUS.id.desc());
query.orderBy(STATUS.entryPublished.desc(), STATUS.id.desc());
}
if (offset > -1) {
@@ -105,7 +104,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.limit(limit);
}
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
setTimeout(query, config.database().queryTimeout());
List<FeedEntryStatus> statuses = query.fetch();
statuses.forEach(s -> s.setMarkable(true));
@@ -165,9 +164,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
if (order != null) {
if (order == ReadingOrder.asc) {
query.orderBy(ENTRY.updated.asc(), ENTRY.id.asc());
query.orderBy(ENTRY.published.asc(), ENTRY.id.asc());
} else {
query.orderBy(ENTRY.updated.desc(), ENTRY.id.desc());
query.orderBy(ENTRY.published.desc(), ENTRY.id.desc());
}
}
@@ -179,7 +178,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.limit(limit);
}
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
setTimeout(query, config.database().queryTimeout());
List<FeedEntryStatus> statuses = new ArrayList<>();
List<Tuple> tuples = query.fetch();
@@ -199,7 +198,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
public UnreadCount getUnreadCount(FeedSubscription sub) {
JPAQuery<Tuple> query = query().select(ENTRY.count(), ENTRY.updated.max())
JPAQuery<Tuple> query = query().select(ENTRY.count(), ENTRY.published.max())
.from(ENTRY)
.leftJoin(ENTRY.statuses, STATUS)
.on(STATUS.subscription.eq(sub))
@@ -208,8 +207,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
Tuple tuple = query.fetchOne();
Long count = tuple.get(ENTRY.count());
Instant updated = tuple.get(ENTRY.updated.max());
return new UnreadCount(sub.getId(), count == null ? 0 : count, updated);
Instant published = tuple.get(ENTRY.published.max());
return new UnreadCount(sub.getId(), count == null ? 0 : count, published);
}
private BooleanBuilder buildUnreadPredicate() {
@@ -217,9 +216,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
or.or(STATUS.read.isNull());
or.or(STATUS.read.isFalse());
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
if (unreadThreshold != null) {
return or.and(ENTRY.updated.goe(unreadThreshold));
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
if (statusesInstantThreshold != null) {
return or.and(ENTRY.published.goe(statusesInstantThreshold));
} else {
return or;
}

View File

@@ -4,24 +4,21 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.QFeedEntryTag;
import com.commafeed.backend.model.User;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
@Inject
public FeedEntryTagDAO(SessionFactory sessionFactory) {
super(sessionFactory);
public FeedEntryTagDAO(EntityManager entityManager) {
super(entityManager, FeedEntryTag.class);
}
public List<String> findByUser(User user) {

View File

@@ -5,12 +5,11 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostCommitInsertEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.persister.entity.EntityPersister;
import com.commafeed.backend.model.AbstractModel;
@@ -23,28 +22,28 @@ import com.commafeed.backend.model.User;
import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPQLQuery;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
private final SessionFactory sessionFactory;
private final EntityManager entityManager;
@Inject
public FeedSubscriptionDAO(SessionFactory sessionFactory) {
super(sessionFactory);
this.sessionFactory = sessionFactory;
public FeedSubscriptionDAO(EntityManager entityManager) {
super(entityManager, FeedSubscription.class);
this.entityManager = entityManager;
}
public void onPostCommitInsert(Consumer<FeedSubscription> consumer) {
sessionFactory.unwrap(SessionFactoryImplementor.class)
entityManager.unwrap(SharedSessionContractImplementor.class)
.getFactory()
.getServiceRegistry()
.getService(EventListenerRegistry.class)
.getEventListenerGroup(EventType.POST_COMMIT_INSERT)
.appendListener(new PostInsertEventListener() {
.appendListener(new PostCommitInsertEventListener() {
@Override
public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof FeedSubscription s) {
@@ -56,6 +55,11 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
@Override
public void onPostInsertCommitFailed(PostInsertEvent event) {
// do nothing
}
});
}

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