mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
301 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54da4e6839 | ||
|
|
3a6b4c588c | ||
|
|
48071b9fd1 | ||
|
|
f519aa039f | ||
|
|
dc3e5476a1 | ||
|
|
903035ecfc | ||
|
|
13ad57da10 | ||
|
|
44bc24c22a | ||
|
|
97f90405fc | ||
|
|
0fc2a0b022 | ||
|
|
89eb641704 | ||
|
|
c53da9f631 | ||
|
|
998868e63a | ||
|
|
93f22d2351 | ||
|
|
c3782bd7d2 | ||
|
|
f330349397 | ||
|
|
99c973c8c2 | ||
|
|
469420b5bf | ||
|
|
bde556d41f | ||
|
|
bf6c2d7beb | ||
|
|
fa62ca21e0 | ||
|
|
7dcf76da84 | ||
|
|
3dc80fa762 | ||
|
|
dbce12492b | ||
|
|
85f5eaffec | ||
|
|
106276351e | ||
|
|
961fb6a464 | ||
|
|
ac3d9ef57f | ||
|
|
3478ee4815 | ||
|
|
3dc02d7ba1 | ||
|
|
c886f8b83c | ||
|
|
4a2154d0b3 | ||
|
|
ba530d5019 | ||
|
|
85b6209c52 | ||
|
|
7ff86a5e31 | ||
|
|
8edd6a1e2d | ||
|
|
6e65ed49e9 | ||
|
|
711b01abfa | ||
|
|
c7014ca2a1 | ||
|
|
a3984cd959 | ||
|
|
8d85b1bcba | ||
|
|
c451eee406 | ||
|
|
8f42135996 | ||
|
|
2c26aeed17 | ||
|
|
e2c4aa998b | ||
|
|
c9e3b7f349 | ||
|
|
ebb4e52ba7 | ||
|
|
1ddfdfb12e | ||
|
|
81f16aea62 | ||
|
|
429ec193c8 | ||
|
|
732b714448 | ||
|
|
82e0405ad9 | ||
|
|
9ef002fcd1 | ||
|
|
ec938e416c | ||
|
|
37cf711cbc | ||
|
|
de441e4ff7 | ||
|
|
46251526b6 | ||
|
|
67eeea0b06 | ||
|
|
b49ccc4cd9 | ||
|
|
8586a8b57b | ||
|
|
d9f63786a8 | ||
|
|
8f0c8b68b9 | ||
|
|
15e574c5c4 | ||
|
|
fe532242b4 | ||
|
|
fb48ff0858 | ||
|
|
8d850639d7 | ||
|
|
ee73195915 | ||
|
|
72d9dad61b | ||
|
|
fde8dab8cd | ||
|
|
dae5efa787 | ||
|
|
3c067140fd | ||
|
|
4ccbe81e87 | ||
|
|
3d5d93bb72 | ||
|
|
4138b6eb9b | ||
|
|
9c39c95a9b | ||
|
|
32b2bf99a4 | ||
|
|
cf459876af | ||
|
|
6698bd74b5 | ||
|
|
c81d06e5f3 | ||
|
|
b12a78dc84 | ||
|
|
b076587e44 | ||
|
|
bb12f16bea | ||
|
|
e80caadd12 | ||
|
|
846d93f2b2 | ||
|
|
0ed6f6ef9c | ||
|
|
15992dcb80 | ||
|
|
1a5c399b54 | ||
|
|
5e92f9ffb8 | ||
|
|
71164d1b69 | ||
|
|
6947670fe6 | ||
|
|
30810e37b9 | ||
|
|
b17b2767b0 | ||
|
|
d37cf5bbcf | ||
|
|
045baba705 | ||
|
|
3623dc8e1d | ||
|
|
2610c37067 | ||
|
|
286b69a646 | ||
|
|
9673f27090 | ||
|
|
0722599f6d | ||
|
|
1df40d8370 | ||
|
|
457e4ec69e | ||
|
|
647310a45f | ||
|
|
e85c92f216 | ||
|
|
d93b0dbfd4 | ||
|
|
b4e61ef547 | ||
|
|
71dffbba46 | ||
|
|
2c0b0c4e3b | ||
|
|
d868e58e1e | ||
|
|
90eb2095bf | ||
|
|
62d3ed16e6 | ||
|
|
74f7c48818 | ||
|
|
23fe9c29ed | ||
|
|
8f7be8278a | ||
|
|
49118b6ea0 | ||
|
|
d97bd04ae2 | ||
|
|
8d11309b64 | ||
|
|
68c24e4cb8 | ||
|
|
4e43e0235f | ||
|
|
62b79a9625 | ||
|
|
cb0706808c | ||
|
|
ffd5704b1e | ||
|
|
3987077e5a | ||
|
|
2e01a76784 | ||
|
|
8254093f5f | ||
|
|
0b06526756 | ||
|
|
06731ae76d | ||
|
|
9a59453792 | ||
|
|
c195a52c89 | ||
|
|
3d7924f953 | ||
|
|
f29efd7fae | ||
|
|
157bff3c83 | ||
|
|
5c17bbc36d | ||
|
|
c85e72e70c | ||
|
|
01150f67e1 | ||
|
|
75aca7aa6f | ||
|
|
affde7e43c | ||
|
|
b9b1b53235 | ||
|
|
708ebb8abc | ||
|
|
83e763df0a | ||
|
|
0ff812c1ea | ||
|
|
3e9dd6d8e2 | ||
|
|
23af73e847 | ||
|
|
e79e4719fd | ||
|
|
23fef98432 | ||
|
|
22478252e7 | ||
|
|
76b1f3cd35 | ||
|
|
420d73ec6a | ||
|
|
e0211cfa0c | ||
|
|
25a92c651c | ||
|
|
0781205c69 | ||
|
|
5102dd5e30 | ||
|
|
6ccfc3fd67 | ||
|
|
2791ed91ab | ||
|
|
f40c198233 | ||
|
|
003dc63121 | ||
|
|
f8ef1e2a99 | ||
|
|
14c7078940 | ||
|
|
074836d3e8 | ||
|
|
0cdbc144b3 | ||
|
|
dc63ec24c0 | ||
|
|
6d4c6c36a5 | ||
|
|
464af5f4d9 | ||
|
|
aa94a46a3d | ||
|
|
8542197dc3 | ||
|
|
64d77eaef4 | ||
|
|
675ef8794c | ||
|
|
4bcdbeb516 | ||
|
|
a9f37739fb | ||
|
|
5ab0fc19da | ||
|
|
7b232425f3 | ||
|
|
c0e7668140 | ||
|
|
ae3f059257 | ||
|
|
d44c7c1e95 | ||
|
|
6cd9d134cf | ||
|
|
6f21ba8afc | ||
|
|
b2fe13c117 | ||
|
|
03ece7a262 | ||
|
|
697fde2d0e | ||
|
|
7f0f85b356 | ||
|
|
a7d41debfe | ||
|
|
57bf758108 | ||
|
|
b37d933047 | ||
|
|
80ffef4555 | ||
|
|
af5a0002aa | ||
|
|
cd24e412e3 | ||
|
|
a073d843ab | ||
|
|
8ccb59ed18 | ||
|
|
e6dc7d2d0d | ||
|
|
f7b21ca3f6 | ||
|
|
df3a1bcdb6 | ||
|
|
5bec494a7c | ||
|
|
d8eef4dd9f | ||
|
|
d80138caf3 | ||
|
|
d26c103aa5 | ||
|
|
249231f57e | ||
|
|
7a838cddad | ||
|
|
477f2cd6db | ||
|
|
9915f05f73 | ||
|
|
0a16bb2fba | ||
|
|
3d4faf2406 | ||
|
|
63de6fe833 | ||
|
|
9c6219a58a | ||
|
|
3e664d4287 | ||
|
|
4c4ffd84f3 | ||
|
|
f555f0e392 | ||
|
|
124166738b | ||
|
|
8b32dcc576 | ||
|
|
105651215a | ||
|
|
d9c6cbd072 | ||
|
|
b4c52e06fe | ||
|
|
2565dfe528 | ||
|
|
b5036c9148 | ||
|
|
e2c8ddb8f7 | ||
|
|
85fbd284fa | ||
|
|
559fb69a64 | ||
|
|
054c76716a | ||
|
|
ba17c9350f | ||
|
|
781015eea4 | ||
|
|
13e5c0e8d2 | ||
|
|
4d88a30848 | ||
|
|
19c03de9e4 | ||
|
|
e3169c9f2d | ||
|
|
e90fb0b56f | ||
|
|
69607a5122 | ||
|
|
21eb8e6d9f | ||
|
|
a28a6b9dc4 | ||
|
|
a9cadbafeb | ||
|
|
d491af5a8d | ||
|
|
39c7934fb8 | ||
|
|
76eba8cc63 | ||
|
|
7549ff2491 | ||
|
|
66cd18bc4b | ||
|
|
44d7a2c340 | ||
|
|
d6ee63a01f | ||
|
|
a495c9cacd | ||
|
|
530185d15c | ||
|
|
8dd28c25a7 | ||
|
|
50884b236c | ||
|
|
da9fe09e58 | ||
|
|
3c24c9aa7a | ||
|
|
9d10a4b46f | ||
|
|
d56ed3bd06 | ||
|
|
41c1c429d0 | ||
|
|
1a3d890b40 | ||
|
|
e3ec9b2ccd | ||
|
|
f69878a242 | ||
|
|
ea766706fb | ||
|
|
306507af80 | ||
|
|
67084783b2 | ||
|
|
7cbb75f717 | ||
|
|
1c335492d5 | ||
|
|
de92e74c8e | ||
|
|
9cbbd30618 | ||
|
|
f14f1493c4 | ||
|
|
e68c8fdbe1 | ||
|
|
e094972aa2 | ||
|
|
ff831c6d2b | ||
|
|
9957cda11a | ||
|
|
6809822000 | ||
|
|
8048b1a9aa | ||
|
|
8557bd018a | ||
|
|
183d5fd162 | ||
|
|
411f86fbeb | ||
|
|
5493046f25 | ||
|
|
42015015a5 | ||
|
|
f3d2808f7d | ||
|
|
906c353a7f | ||
|
|
93dea83cd3 | ||
|
|
1fc76ce1ad | ||
|
|
a337b01bc7 | ||
|
|
689e5c0004 | ||
|
|
d5a3c81c85 | ||
|
|
8230fde5d2 | ||
|
|
b35513ea84 | ||
|
|
42a7785ca1 | ||
|
|
ea5ee4f04f | ||
|
|
3e14b12d4f | ||
|
|
78cc30f828 | ||
|
|
6091c84e60 | ||
|
|
6ea95ad254 | ||
|
|
7f888d926e | ||
|
|
5e4e02474f | ||
|
|
bff8611b42 | ||
|
|
f674048af3 | ||
|
|
0265c24cf9 | ||
|
|
f8c3a229ec | ||
|
|
c424f40420 | ||
|
|
b77666cfe5 | ||
|
|
193d1604d9 | ||
|
|
4efc6296b5 | ||
|
|
f753a4bdda | ||
|
|
afaaaf9657 | ||
|
|
4d46896bf0 | ||
|
|
2ad28c927f | ||
|
|
b9680a66ef | ||
|
|
4f687d5857 | ||
|
|
9cca026834 | ||
|
|
058a9cd192 | ||
|
|
57d2ede86e | ||
|
|
e3abea4ec5 | ||
|
|
b831f1f35c |
270
.github/workflows/ci.yml
vendored
270
.github/workflows/ci.yml
vendored
@@ -1,180 +1,224 @@
|
|||||||
name: ci
|
name: ci
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on: [ push ]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_VERSION: 21
|
JAVA_VERSION: 21
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux:
|
build:
|
||||||
runs-on: ubuntu-latest
|
if: github.event_name != 'pull_request' || github.actor != 'renovate[bot]' # renovate already triggers the build on pushes
|
||||||
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
os: [ "ubuntu-latest", "ubuntu-22.04-arm", "windows-latest" ]
|
||||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
# Checkout
|
# Checkout
|
||||||
- name: Configure git to checkout as-is
|
- name: Configure git to checkout as-is
|
||||||
run: git config --global core.autocrlf false
|
run: git config --global core.autocrlf false
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
- name: Set up GraalVM
|
- name: Set up GraalVM
|
||||||
uses: graalvm/setup-graalvm@v1
|
uses: graalvm/setup-graalvm@b0cb26a8da53cb3e97cdc0c827d8e3071240e730 # v1
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: "graalvm"
|
distribution: "graalvm"
|
||||||
cache: "maven"
|
cache: "maven"
|
||||||
|
|
||||||
|
- name: Install Playwright dependencies
|
||||||
|
run: sudo apt-get install -y libgbm1
|
||||||
|
if: matrix.os != 'windows-latest'
|
||||||
|
|
||||||
# Build & Test
|
# Build & Test
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }}
|
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
|
||||||
|
|
||||||
# Upload artifacts
|
# Upload artifacts
|
||||||
|
- name: Upload cross-platform app
|
||||||
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||||
|
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
||||||
|
with:
|
||||||
|
name: commafeed-${{ matrix.database }}-jvm
|
||||||
|
path: commafeed-server/target/commafeed-*.zip
|
||||||
|
|
||||||
- name: Upload native executable
|
- name: Upload native executable
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||||
with:
|
with:
|
||||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
path: commafeed-server/target/commafeed-*-runner.exe
|
path: commafeed-server/target/commafeed-*-runner*
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
|
||||||
|
|
||||||
|
- name: Install required packages
|
||||||
|
run: sudo apt-get install -y rename unzip
|
||||||
|
|
||||||
|
# Prepare artifacts
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||||
|
with:
|
||||||
|
pattern: commafeed-${{ matrix.database }}-*
|
||||||
|
path: ./artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set the exec flag on the native executables
|
||||||
|
run: chmod +x artifacts/*-runner
|
||||||
|
|
||||||
|
- name: Rename native executables to match buildx TARGETARCH
|
||||||
|
run: |
|
||||||
|
rename 's/x86_64/amd64/g' artifacts/*
|
||||||
|
rename 's/aarch_64/arm64/g' artifacts/*
|
||||||
|
|
||||||
|
- name: Unzip jvm package
|
||||||
|
run: |
|
||||||
|
unzip artifacts/*-jvm.zip -d artifacts/extracted-jvm-package
|
||||||
|
rename 's/commafeed-.*/quarkus-app/g' artifacts/extracted-jvm-package/*
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||||
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
## build but don't push for PRs and renovate
|
||||||
|
- name: Docker build - native
|
||||||
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
|
push: false
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
- name: Docker build - jvm
|
||||||
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
|
push: false
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
## build and push tag
|
||||||
|
- name: Docker build and push tag - native
|
||||||
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
|
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
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@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
|
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
tags: |
|
||||||
|
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||||
|
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||||
|
|
||||||
|
## build and push master
|
||||||
|
- name: Docker build and push master - native
|
||||||
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
if: ${{ github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
|
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
tags: athou/commafeed:master-${{ matrix.database }}
|
||||||
|
|
||||||
|
- name: Docker build and push master - jvm
|
||||||
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||||
|
if: ${{ github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
|
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-linux
|
- build
|
||||||
- build-windows
|
- docker
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
if: github.ref_type == 'tag'
|
if: github.ref_type == 'tag'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||||
with:
|
with:
|
||||||
pattern: commafeed-*
|
pattern: commafeed-*
|
||||||
path: ./artifacts
|
path: ./artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set the exec flag on the native executables
|
||||||
|
run: chmod +x artifacts/*-runner
|
||||||
|
|
||||||
- name: Extract Changelog Entry
|
- name: Extract Changelog Entry
|
||||||
uses: mindsers/changelog-reader-action@v2
|
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2
|
||||||
id: changelog_reader
|
id: changelog_reader
|
||||||
with:
|
with:
|
||||||
version: ${{ github.ref_name }}
|
version: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1
|
||||||
with:
|
with:
|
||||||
name: CommaFeed ${{ github.ref_name }}
|
name: CommaFeed ${{ github.ref_name }}
|
||||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||||
artifacts: ./artifacts/*
|
artifacts: ./artifacts/*
|
||||||
|
|
||||||
- name: Update Docker Hub Description
|
- name: Update Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v4
|
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [5.6.0]
|
||||||
|
|
||||||
|
- To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is now true. This means feeds no longer refresh exactly every 5 minutes (the default value of `commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new `commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active feeds refresh more often (#1677)
|
||||||
|
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
|
||||||
|
- Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses` setting if you subscribe to feeds only available on your local network and you trust all your users
|
||||||
|
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does not contain a "Retry-After" header
|
||||||
|
|
||||||
|
## [5.5.0]
|
||||||
|
|
||||||
|
- CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this header
|
||||||
|
- Audio enclosures (e.g. podcasts) now fill available entry width
|
||||||
|
- Fix an issue with some labels not correctly internationalized
|
||||||
|
|
||||||
|
## [5.4.0]
|
||||||
|
|
||||||
|
- An arm64 native executable is now available for download on the releases page
|
||||||
|
- The native executable Docker image now supports arm64
|
||||||
|
- Fixed an issue with feeds that declared an invalid DOCTYPE (#1260)
|
||||||
|
|
||||||
|
## [5.3.6]
|
||||||
|
|
||||||
|
- Ignore invalid Cache-Control header values (#1619)
|
||||||
|
|
||||||
|
## [5.3.5]
|
||||||
|
|
||||||
|
- Fixed an issue with the aspect ratio of images of some feeds (#1595)
|
||||||
|
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age property (#1615)
|
||||||
|
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may need to update it to the most recent patch version to support `-proc:full` (#1618)
|
||||||
|
|
||||||
|
## [5.3.4]
|
||||||
|
|
||||||
|
- Added support for Internationalized Domain Names (#1588)
|
||||||
|
|
||||||
|
## [5.3.3]
|
||||||
|
|
||||||
|
- Removed image bottom margins (#1587)
|
||||||
|
|
||||||
|
## [5.3.2]
|
||||||
|
|
||||||
|
- Fixed an issue that could cause some images from not being rendered correctly (#1587)
|
||||||
|
|
||||||
## [5.3.1]
|
## [5.3.1]
|
||||||
|
|
||||||
- Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
|
- Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -94,13 +94,13 @@ There are multiple ways to configure CommaFeed:
|
|||||||
|
|
||||||
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
|
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
|
||||||
directory (keys in kebab-case)
|
directory (keys in kebab-case)
|
||||||
- Command line arguments prefixed with `-D` (keys in kebab-case)
|
- Command line arguments each prefixed with `-D` (keys in kebab-case)
|
||||||
- Environment variables (keys in UPPER_CASE)
|
- Environment variables (keys in UPPER_CASE)
|
||||||
- a `.env` file in the working directory (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.
|
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.
|
All [CommaFeed settings](commafeed-server/doc/commafeed.md) 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,
|
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
|
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||||
@@ -114,18 +114,18 @@ The default user is `admin` and the default password is `admin`.
|
|||||||
|
|
||||||
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
|
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
|
||||||
|
|
||||||
### Memory management (`jvm` package only)
|
### Memory management
|
||||||
|
|
||||||
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
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.
|
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
||||||
This can be problematic on systems with limited memory.
|
This can be problematic on systems with limited memory.
|
||||||
|
|
||||||
#### Hard limit
|
#### Hard limit (`native` and `jvm` packages)
|
||||||
|
|
||||||
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
|
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
|
||||||
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
|
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
|
||||||
|
|
||||||
#### Dynamic sizing
|
#### Dynamic sizing (`jvm` package)
|
||||||
|
|
||||||
In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
|
In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
|
||||||
following parameters:
|
following parameters:
|
||||||
@@ -137,7 +137,7 @@ and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-
|
|||||||
more
|
more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
#### OpenJ9
|
#### OpenJ9 (`jvm` package)
|
||||||
|
|
||||||
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
|
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
|
||||||
slightly slower throughput.
|
slightly slower throughput.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 4,
|
"indentWidth": 4,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>CommaFeed</title>
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="custom_css.css" />
|
||||||
|
<script type="text/javascript" src="custom_js.js"></script>
|
||||||
|
|
||||||
|
<title>CommaFeed</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
5536
commafeed-client/package-lock.json
generated
5536
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,65 +15,68 @@
|
|||||||
"i18n:extract": "lingui extract --clean"
|
"i18n:extract": "lingui extract --clean"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.14.0",
|
||||||
"@fontsource/open-sans": "^5.1.0",
|
"@fontsource/open-sans": "^5.1.1",
|
||||||
"@lingui/core": "^4.11.4",
|
"@mantine/core": "^7.16.3",
|
||||||
"@lingui/macro": "^4.11.4",
|
"@mantine/form": "^7.16.3",
|
||||||
"@lingui/react": "^4.11.4",
|
"@mantine/hooks": "^7.16.3",
|
||||||
"@mantine/core": "^7.13.2",
|
"@mantine/modals": "^7.16.3",
|
||||||
"@mantine/form": "^7.13.2",
|
"@mantine/notifications": "^7.16.3",
|
||||||
"@mantine/hooks": "^7.13.2",
|
"@mantine/spotlight": "^7.16.3",
|
||||||
"@mantine/modals": "^7.13.2",
|
"@lingui/core": "^5.2.0",
|
||||||
"@mantine/notifications": "^7.13.2",
|
"@lingui/react": "^5.2.0",
|
||||||
"@mantine/spotlight": "^7.13.2",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"axios": "^1.7.9",
|
||||||
"axios": "^1.7.7",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"interweave": "^13.1.0",
|
"interweave": "^13.1.1",
|
||||||
"monaco-editor": "^0.52.0",
|
"monaco-editor": "^0.52.2",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"react": "^18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-async-hook": "^4.0.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.0.0",
|
||||||
"react-draggable": "^4.4.6",
|
"react-draggable": "^4.4.6",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-icons": "^5.3.0",
|
|
||||||
"react-infinite-scroller": "^1.2.6",
|
"react-infinite-scroller": "^1.2.6",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^7.1.5",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.2",
|
||||||
"redoc": "^2.1.5",
|
"redoc": "^2.4.0",
|
||||||
|
"style-to-object": "^1.0.8",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tinycon": "^0.6.8",
|
"tinycon": "^0.6.8",
|
||||||
"tss-react": "^4.9.13",
|
"tss-react": "^4.9.15",
|
||||||
"websocket-heartbeat-js": "^1.1.3"
|
"websocket-heartbeat-js": "^1.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.3",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@lingui/cli": "^4.11.4",
|
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
|
||||||
"@lingui/vite-plugin": "^4.11.4",
|
"@lingui/cli": "^5.2.0",
|
||||||
|
"@lingui/vite-plugin": "^5.2.0",
|
||||||
"@types/mousetrap": "^1.6.15",
|
"@types/mousetrap": "^1.6.15",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/react-helmet": "^6.1.11",
|
|
||||||
"@types/react-infinite-scroller": "^1.2.5",
|
"@types/react-infinite-scroller": "^1.2.5",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@types/tinycon": "^0.6.5",
|
"@types/tinycon": "^0.6.7",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^26.0.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.8",
|
"vite": "^6.1.0",
|
||||||
"vite-plugin-checker": "^0.8.0",
|
"vite-plugin-checker": "^0.8.0",
|
||||||
"vite-tsconfig-paths": "^5.0.1",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^2.1.2",
|
"vitest": "^3.0.5",
|
||||||
"vitest-mock-extended": "^2.0.2"
|
"vitest-mock-extended": "^2.0.2"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"react-infinite-scroller": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<name>CommaFeed Client</name>
|
<name>CommaFeed Client</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- renovate: datasource=node-version depName=node -->
|
<!-- renovate: datasource=node-version depName=node -->
|
||||||
<node.version>v20.18.0</node.version>
|
<node.version>v22.14.0</node.version>
|
||||||
<!-- renovate: datasource=npm depName=npm -->
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
<npm.version>10.9.0</npm.version>
|
<npm.version>11.1.0</npm.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { RegistrationPage } from "pages/auth/RegistrationPage"
|
|||||||
import React, { useEffect } from "react"
|
import React, { useEffect } from "react"
|
||||||
import { isSafari } from "react-device-detect"
|
import { isSafari } from "react-device-detect"
|
||||||
import ReactGA from "react-ga4"
|
import ReactGA from "react-ga4"
|
||||||
import { Helmet } from "react-helmet"
|
|
||||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||||
import Tinycon from "tinycon"
|
import Tinycon from "tinycon"
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ function GoogleAnalyticsHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||||
return <Helmet title={enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"} />
|
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
|
||||||
}
|
}
|
||||||
|
|
||||||
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||||
@@ -170,15 +169,6 @@ function BrowserExtensionBadgeUnreadCountHandler() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomCode() {
|
|
||||||
return (
|
|
||||||
<Helmet>
|
|
||||||
<link rel="stylesheet" type="text/css" href="custom_css.css" />
|
|
||||||
<script type="text/javascript" src="custom_js.js" />
|
|
||||||
</Helmet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useI18n()
|
useI18n()
|
||||||
const root = useAppSelector(state => state.tree.rootCategory)
|
const root = useAppSelector(state => state.tree.rootCategory)
|
||||||
@@ -202,7 +192,6 @@ export function App() {
|
|||||||
<GoogleAnalyticsHandler />
|
<GoogleAnalyticsHandler />
|
||||||
<RedirectHandler />
|
<RedirectHandler />
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
<CustomCode />
|
|
||||||
{/* disable pull-to-refresh as it messes with vertical scrolling
|
{/* disable pull-to-refresh as it messes with vertical scrolling
|
||||||
safari behaves weirdly when overscroll-behavior is set to none so we disable it only for other browsers
|
safari behaves weirdly when overscroll-behavior is set to none so we disable it only for other browsers
|
||||||
https://github.com/Athou/commafeed/issues/1168
|
https://github.com/Athou/commafeed/issues/1168
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { t } from "@lingui/macro"
|
|
||||||
import type { IconType } from "react-icons"
|
import type { IconType } from "react-icons"
|
||||||
import { FaAt } from "react-icons/fa"
|
import { FaAt } from "react-icons/fa"
|
||||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
|
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
|
||||||
import type { Category, Entry, SharingSettings } from "./types"
|
import type { Category, Entry, SharingSettings } from "./types"
|
||||||
|
|
||||||
const categories: Record<string, Category> = {
|
const categories: Record<string, Omit<Category, "name">> = {
|
||||||
all: {
|
all: {
|
||||||
id: "all",
|
id: "all",
|
||||||
name: t`All`,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
children: [],
|
children: [],
|
||||||
feeds: [],
|
feeds: [],
|
||||||
@@ -15,7 +13,6 @@ const categories: Record<string, Category> = {
|
|||||||
},
|
},
|
||||||
starred: {
|
starred: {
|
||||||
id: "starred",
|
id: "starred",
|
||||||
name: t`Starred`,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
children: [],
|
children: [],
|
||||||
feeds: [],
|
feeds: [],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { flushSync } from "react-dom"
|
|||||||
|
|
||||||
const getEndpoint = (sourceType: EntrySourceType) =>
|
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||||
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||||
|
|
||||||
export const loadEntries = createAppAsyncThunk(
|
export const loadEntries = createAppAsyncThunk(
|
||||||
"entries/load",
|
"entries/load",
|
||||||
async (
|
async (
|
||||||
@@ -28,6 +29,7 @@ export const loadEntries = createAppAsyncThunk(
|
|||||||
return result.data
|
return result.data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
|
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const { source } = state.entries
|
const { source } = state.entries
|
||||||
@@ -37,6 +39,7 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
|
|||||||
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
|
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
|
||||||
return result.data
|
return result.data
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
||||||
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||||
order: state.user.settings?.readingOrder,
|
order: state.user.settings?.readingOrder,
|
||||||
@@ -46,15 +49,18 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
|
|||||||
tag: source.type === "tag" ? source.id : undefined,
|
tag: source.type === "tag" ? source.id : undefined,
|
||||||
keywords: state.entries.search,
|
keywords: state.entries.search,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => {
|
export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
|
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
thunkApi.dispatch(setSearch(arg))
|
thunkApi.dispatch(setSearch(arg))
|
||||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const markEntry = createAppAsyncThunk(
|
export const markEntry = createAppAsyncThunk(
|
||||||
"entries/entry/mark",
|
"entries/entry/mark",
|
||||||
(arg: { entry: Entry; read: boolean }) => {
|
(arg: { entry: Entry; read: boolean }) => {
|
||||||
@@ -67,6 +73,7 @@ export const markEntry = createAppAsyncThunk(
|
|||||||
condition: arg => arg.entry.markable && arg.entry.read !== arg.read,
|
condition: arg => arg.entry.markable && arg.entry.read !== arg.read,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const markMultipleEntries = createAppAsyncThunk(
|
export const markMultipleEntries = createAppAsyncThunk(
|
||||||
"entries/entry/markMultiple",
|
"entries/entry/markMultiple",
|
||||||
async (
|
async (
|
||||||
@@ -84,6 +91,7 @@ export const markMultipleEntries = createAppAsyncThunk(
|
|||||||
thunkApi.dispatch(reloadTree())
|
thunkApi.dispatch(reloadTree())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const { entries } = state.entries
|
const { entries } = state.entries
|
||||||
@@ -98,6 +106,7 @@ export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const markAllEntries = createAppAsyncThunk(
|
export const markAllEntries = createAppAsyncThunk(
|
||||||
"entries/entry/markAll",
|
"entries/entry/markAll",
|
||||||
async (
|
async (
|
||||||
@@ -113,6 +122,7 @@ export const markAllEntries = createAppAsyncThunk(
|
|||||||
thunkApi.dispatch(reloadTree())
|
thunkApi.dispatch(reloadTree())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const starEntry = createAppAsyncThunk(
|
export const starEntry = createAppAsyncThunk(
|
||||||
"entries/entry/star",
|
"entries/entry/star",
|
||||||
(arg: { entry: Entry; starred: boolean }) => {
|
(arg: { entry: Entry; starred: boolean }) => {
|
||||||
@@ -126,6 +136,7 @@ export const starEntry = createAppAsyncThunk(
|
|||||||
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
|
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectEntry = createAppAsyncThunk(
|
export const selectEntry = createAppAsyncThunk(
|
||||||
"entries/entry/select",
|
"entries/entry/select",
|
||||||
(
|
(
|
||||||
@@ -191,6 +202,7 @@ export const selectEntry = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||||
const offset = (header?.bottom ?? 0) + margin
|
const offset = (header?.bottom ?? 0) + margin
|
||||||
@@ -228,6 +240,7 @@ export const selectPreviousEntry = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectNextEntry = createAppAsyncThunk(
|
export const selectNextEntry = createAppAsyncThunk(
|
||||||
"entries/entry/selectNext",
|
"entries/entry/selectNext",
|
||||||
async (
|
async (
|
||||||
@@ -261,6 +274,7 @@ export const selectNextEntry = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
|
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
|
||||||
await client.entry.tag(arg)
|
await client.entry.tag(arg)
|
||||||
thunkApi.dispatch(reloadTags())
|
thunkApi.dispatch(reloadTags())
|
||||||
|
|||||||
@@ -3,43 +3,55 @@ import { Constants } from "app/constants"
|
|||||||
import { redirectTo } from "app/redirect/slice"
|
import { redirectTo } from "app/redirect/slice"
|
||||||
|
|
||||||
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||||
|
|
||||||
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
||||||
export const redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
|
|
||||||
thunkApi.dispatch(redirectTo("/passwordRecovery"))
|
|
||||||
)
|
|
||||||
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
|
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
|
||||||
|
|
||||||
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
|
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
|
||||||
const { source } = thunkApi.getState().entries
|
const { source } = thunkApi.getState().entries
|
||||||
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToRootCategory = createAppAsyncThunk(
|
export const redirectToRootCategory = createAppAsyncThunk(
|
||||||
"redirect/category/root",
|
"redirect/category/root",
|
||||||
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
|
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
|
||||||
|
|
||||||
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
|
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
||||||
|
|
||||||
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
||||||
|
|
||||||
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
|
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
|
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
|
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
|
||||||
|
|
||||||
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
|
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
import { createAppAsyncThunk } from "app/async-thunk"
|
import { createAppAsyncThunk } from "app/async-thunk"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
|
import { incrementUnreadCount } from "app/tree/slice"
|
||||||
import type { CollapseRequest } from "app/types"
|
import type { CollapseRequest } from "app/types"
|
||||||
|
import { flattenCategoryTree } from "app/utils"
|
||||||
|
|
||||||
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
|
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
|
||||||
|
|
||||||
export const collapseTreeCategory = createAppAsyncThunk(
|
export const collapseTreeCategory = createAppAsyncThunk(
|
||||||
"tree/category/collapse",
|
"tree/category/collapse",
|
||||||
async (req: CollapseRequest) => await client.category.collapse(req)
|
async (req: CollapseRequest) => await client.category.collapse(req)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const newFeedEntriesDiscovered = createAppAsyncThunk(
|
||||||
|
"tree/new-feed-entries-discovered",
|
||||||
|
async ({ feedId, amount }: { feedId: number; amount: number }, thunkApi) => {
|
||||||
|
const root = thunkApi.getState().tree.rootCategory
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
const feed = flattenCategoryTree(root)
|
||||||
|
.flatMap(c => c.feeds)
|
||||||
|
.some(f => f.id === feedId)
|
||||||
|
if (!feed) {
|
||||||
|
// feed not found in the tree, reload the tree completely
|
||||||
|
thunkApi.dispatch(reloadTree())
|
||||||
|
} else {
|
||||||
|
thunkApi.dispatch(
|
||||||
|
incrementUnreadCount({
|
||||||
|
feedId,
|
||||||
|
amount,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import { showNotification } from "@mantine/notifications"
|
import { showNotification } from "@mantine/notifications"
|
||||||
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
|
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
|
||||||
|
|||||||
@@ -4,45 +4,55 @@ import { reloadEntries } from "app/entries/thunks"
|
|||||||
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
||||||
|
|
||||||
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
||||||
|
|
||||||
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
|
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
|
||||||
|
|
||||||
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
|
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
|
||||||
|
|
||||||
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
|
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, readingMode })
|
client.user.saveSettings({ ...settings, readingMode })
|
||||||
thunkApi.dispatch(reloadEntries())
|
thunkApi.dispatch(reloadEntries())
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
|
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, readingOrder })
|
client.user.saveSettings({ ...settings, readingOrder })
|
||||||
thunkApi.dispatch(reloadEntries())
|
thunkApi.dispatch(reloadEntries())
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
|
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, language })
|
client.user.saveSettings({ ...settings, language })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
|
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
|
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, showRead })
|
client.user.saveSettings({ ...settings, showRead })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
|
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollMarks })
|
client.user.saveSettings({ ...settings, scrollMarks })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollMode })
|
client.user.saveSettings({ ...settings, scrollMode })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
||||||
"settings/entriesToKeepOnTopWhenScrolling",
|
"settings/entriesToKeepOnTopWhenScrolling",
|
||||||
(entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
|
(entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
|
||||||
@@ -51,6 +61,7 @@ export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
|||||||
client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
|
client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||||
"settings/starIconDisplayMode",
|
"settings/starIconDisplayMode",
|
||||||
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||||
@@ -59,6 +70,7 @@ export const changeStarIconDisplayMode = createAppAsyncThunk(
|
|||||||
client.user.saveSettings({ ...settings, starIconDisplayMode })
|
client.user.saveSettings({ ...settings, starIconDisplayMode })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
||||||
"settings/externalLinkIconDisplayMode",
|
"settings/externalLinkIconDisplayMode",
|
||||||
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
|
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||||
@@ -67,6 +79,7 @@ export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
|||||||
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
|
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||||
"settings/markAllAsReadConfirmation",
|
"settings/markAllAsReadConfirmation",
|
||||||
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
||||||
@@ -75,26 +88,31 @@ export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
|||||||
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
|
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
|
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, customContextMenu })
|
client.user.saveSettings({ ...settings, customContextMenu })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
|
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, mobileFooter })
|
client.user.saveSettings({ ...settings, mobileFooter })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
|
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, unreadCountTitle })
|
client.user.saveSettings({ ...settings, unreadCountTitle })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
|
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, unreadCountFavicon })
|
client.user.saveSettings({ ...settings, unreadCountFavicon })
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeSharingSetting = createAppAsyncThunk(
|
export const changeSharingSetting = createAppAsyncThunk(
|
||||||
"settings/sharingSetting",
|
"settings/sharingSetting",
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Alert as MantineAlert } from "@mantine/core"
|
import { Box, Alert as MantineAlert } from "@mantine/core"
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Dialog, Text } from "@mantine/core"
|
import { Box, Dialog, Text } from "@mantine/core"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { setAnnouncementHash } from "app/user/slice"
|
import { setAnnouncementHash } from "app/user/slice"
|
||||||
|
|||||||
4
commafeed-client/src/components/DisablePullToRefresh.css
Normal file
4
commafeed-client/src/components/DisablePullToRefresh.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
@@ -1,15 +1,4 @@
|
|||||||
import { Helmet } from "react-helmet"
|
|
||||||
|
|
||||||
export const DisablePullToRefresh = () => {
|
export const DisablePullToRefresh = () => {
|
||||||
return (
|
import("./DisablePullToRefresh.css")
|
||||||
<Helmet>
|
return <></>
|
||||||
<style type="text/css">
|
|
||||||
{`
|
|
||||||
html, body {
|
|
||||||
overscroll-behavior: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</Helmet>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ImageWithPlaceholderWhileLoadingProps {
|
|||||||
title?: string
|
title?: string
|
||||||
width?: number
|
width?: number
|
||||||
height?: number | "auto"
|
height?: number | "auto"
|
||||||
|
style?: React.CSSProperties
|
||||||
placeholderWidth?: number
|
placeholderWidth?: number
|
||||||
placeholderHeight?: number
|
placeholderHeight?: number
|
||||||
placeholderBackgroundColor?: string
|
placeholderBackgroundColor?: string
|
||||||
@@ -42,6 +43,7 @@ export function ImageWithPlaceholderWhileLoading({
|
|||||||
src,
|
src,
|
||||||
title,
|
title,
|
||||||
width,
|
width,
|
||||||
|
style,
|
||||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||||
const { classes } = useStyles({
|
const { classes } = useStyles({
|
||||||
placeholderWidth,
|
placeholderWidth,
|
||||||
@@ -68,7 +70,11 @@ export function ImageWithPlaceholderWhileLoading({
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onLoad={() => setLoading(false)}
|
onLoad={() => setLoading(false)}
|
||||||
style={{ display: loading ? "none" : "block" }}
|
style={{
|
||||||
|
...style,
|
||||||
|
display: loading ? "none" : (style?.display ?? "initial"),
|
||||||
|
height: style?.width ? "auto" : style?.height,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
||||||
import { useOs } from "@mantine/hooks"
|
import { useOs } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Tooltip } from "@mantine/core"
|
import { Tooltip } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import { TypographyStylesProvider } from "@mantine/core"
|
import { TypographyStylesProvider } from "@mantine/core"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
import { tss } from "tss"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is used to provide basic styles to html typography elements.
|
* This component is used to provide basic styles to html typography elements.
|
||||||
*
|
*
|
||||||
* see https://mantine.dev/core/typography-styles-provider/
|
* see https://mantine.dev/core/typography-styles-provider/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const useStyles = tss.create(() => ({
|
||||||
|
// override mantine default typography styles
|
||||||
|
content: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
"& img": {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
|
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
|
||||||
return <TypographyStylesProvider pl={0}>{props.children}</TypographyStylesProvider>
|
const { classes } = useStyles()
|
||||||
|
return <TypographyStylesProvider className={classes.content}>{props.children}</TypographyStylesProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
|||||||
import escapeStringRegexp from "escape-string-regexp"
|
import escapeStringRegexp from "escape-string-regexp"
|
||||||
import { type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
import { type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
import styleToObject from "style-to-object"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
export interface ContentProps {
|
export interface ContentProps {
|
||||||
@@ -42,6 +43,7 @@ const transform: TransformCallback = node => {
|
|||||||
const nodeHeight = node.getAttribute("height")
|
const nodeHeight = node.getAttribute("height")
|
||||||
const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
|
const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
|
||||||
const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
|
const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
|
||||||
|
const style = styleToObject(node.getAttribute("style") ?? "") ?? undefined
|
||||||
const placeholderSize = calculatePlaceholderSize({
|
const placeholderSize = calculatePlaceholderSize({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -55,6 +57,7 @@ const transform: TransformCallback = node => {
|
|||||||
title={title}
|
title={title}
|
||||||
width={width}
|
width={width}
|
||||||
height="auto"
|
height="auto"
|
||||||
|
style={style}
|
||||||
placeholderWidth={placeholderSize.width}
|
placeholderWidth={placeholderSize.width}
|
||||||
placeholderHeight={placeholderSize.height}
|
placeholderHeight={placeholderSize.height}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function Enclosure(props: {
|
|||||||
)}
|
)}
|
||||||
{hasAudio && (
|
{hasAudio && (
|
||||||
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
|
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
|
||||||
<audio controls>
|
<audio controls style={{ width: "100%" }}>
|
||||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||||
</audio>
|
</audio>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { openModal } from "@mantine/modals"
|
import { openModal } from "@mantine/modals"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Group } from "@mantine/core"
|
import { Group } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
import { Select, type SelectProps } from "@mantine/core"
|
import { Select, type SelectProps } from "@mantine/core"
|
||||||
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||||
import { isNotEmpty, useForm } from "@mantine/form"
|
import { isNotEmpty, useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
|
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
@@ -39,8 +39,8 @@ export function Subscribe() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
||||||
onSuccess: sub => {
|
onSuccess: async sub => {
|
||||||
dispatch(reloadTree())
|
await dispatch(reloadTree())
|
||||||
dispatch(redirectToFeed(sub.data))
|
dispatch(redirectToFeed(sub.data))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { markEntry } from "app/entries/thunks"
|
import { markEntry } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { ActionIcon, Tooltip } from "@mantine/core"
|
import { ActionIcon, Tooltip } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { starEntry } from "app/entries/thunks"
|
import { starEntry } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
|
|
||||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||||
import { markAllEntries } from "app/entries/thunks"
|
import { markAllEntries } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
Divider,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, Group, Stack } from "@mantine/core"
|
import { Box, Button, Group, Stack } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Divider, Group, NumberInput, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
import { Divider, Group, NumberInput, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||||
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { openConfirmModal } from "@mantine/modals"
|
import { openConfirmModal } from "@mantine/modals"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Stack } from "@mantine/core"
|
import { Box, Stack } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { TextInput } from "@mantine/core"
|
import { TextInput } from "@mantine/core"
|
||||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||||
import { redirectToFeed } from "app/redirect/thunks"
|
import { redirectToFeed } from "app/redirect/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { setWebSocketConnected } from "app/server/slice"
|
import { setWebSocketConnected } from "app/server/slice"
|
||||||
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
|
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { incrementUnreadCount } from "app/tree/slice"
|
import { newFeedEntriesDiscovered } from "app/tree/thunks"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ const handleMessage = (dispatch: AppDispatch, message: string) => {
|
|||||||
const type = parts[0]
|
const type = parts[0]
|
||||||
if (type === "new-feed-entries") {
|
if (type === "new-feed-entries") {
|
||||||
dispatch(
|
dispatch(
|
||||||
incrementUnreadCount({
|
newFeedEntriesDiscovered({
|
||||||
feedId: +parts[1],
|
feedId: +parts[1],
|
||||||
amount: +parts[2],
|
amount: +parts[2],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ msgstr "浏览器扩展"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Browser tab"
|
msgid "Browser tab"
|
||||||
msgstr ""
|
msgstr "浏览器标签页"
|
||||||
|
|
||||||
#: src/components/admin/UserEdit.tsx
|
#: src/components/admin/UserEdit.tsx
|
||||||
#: src/components/content/add/AddCategory.tsx
|
#: src/components/content/add/AddCategory.tsx
|
||||||
@@ -323,7 +323,7 @@ msgstr "输入您当前的密码以更改配置文件设置"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Entries to keep above the selected entry when scrolling"
|
msgid "Entries to keep above the selected entry when scrolling"
|
||||||
msgstr ""
|
msgstr "滚动时固定在顶部的条目"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Entry headers"
|
msgid "Entry headers"
|
||||||
@@ -378,7 +378,7 @@ msgstr "过滤表达式"
|
|||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Force fetching feeds is not yet available."
|
msgid "Force fetching feeds is not yet available."
|
||||||
msgstr ""
|
msgstr "强制获取订阅源功能不可用。"
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
@@ -844,11 +844,11 @@ msgstr "显示星标图标"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Show unread count in tab favicon"
|
msgid "Show unread count in tab favicon"
|
||||||
msgstr ""
|
msgstr "在标签页图标上显示未读数量"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Show unread count in tab title"
|
msgid "Show unread count in tab title"
|
||||||
msgstr ""
|
msgstr "在标签页标题中显示未读数量"
|
||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Button, Container, Group, Text, Title } from "@mantine/core"
|
import { Box, Button, Container, Group, Text, Title } from "@mantine/core"
|
||||||
import { TbRefresh } from "react-icons/tb"
|
import { TbRefresh } from "react-icons/tb"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, useMantineColorScheme } from "@mantine/core"
|
import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, useMantineColorScheme } from "@mantine/core"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
|
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToApiDocumentation } from "app/redirect/thunks"
|
import { redirectToApiDocumentation } from "app/redirect/thunks"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Container, Tabs } from "@mantine/core"
|
import { Container, Tabs } from "@mantine/core"
|
||||||
import { AddCategory } from "components/content/add/AddCategory"
|
import { AddCategory } from "components/content/add/AddCategory"
|
||||||
import { ImportOpml } from "components/content/add/ImportOpml"
|
import { ImportOpml } from "components/content/add/ImportOpml"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { openConfirmModal } from "@mantine/modals"
|
import { openConfirmModal } from "@mantine/modals"
|
||||||
@@ -19,6 +21,7 @@ import { useParams } from "react-router-dom"
|
|||||||
|
|
||||||
export function CategoryDetailsPage() {
|
export function CategoryDetailsPage() {
|
||||||
const { id = Constants.categories.all.id } = useParams()
|
const { id = Constants.categories.all.id } = useParams()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -26,7 +29,7 @@ export function CategoryDetailsPage() {
|
|||||||
const query = useAsync(async () => await client.category.getRoot(), [])
|
const query = useAsync(async () => await client.category.getRoot(), [])
|
||||||
const category =
|
const category =
|
||||||
id === Constants.categories.starred.id
|
id === Constants.categories.starred.id
|
||||||
? Constants.categories.starred
|
? { ...Constants.categories.starred, name: _(msg`Starred`) }
|
||||||
: query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
|
: query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
|
||||||
|
|
||||||
const form = useForm<CategoryModificationRequest>()
|
const form = useForm<CategoryModificationRequest>()
|
||||||
@@ -63,14 +66,14 @@ export function CategoryDetailsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!category) return
|
if (!category?.id) return
|
||||||
setValues({
|
setValues({
|
||||||
id: +category.id,
|
id: +category.id,
|
||||||
name: category.name,
|
name: category.name,
|
||||||
parentId: category.parentId,
|
parentId: category.parentId,
|
||||||
position: category.position,
|
position: category.position,
|
||||||
})
|
})
|
||||||
}, [setValues, category])
|
}, [setValues, category?.id, category?.name, category?.parentId, category?.position])
|
||||||
|
|
||||||
const editable = id !== Constants.categories.all.id && id !== Constants.categories.starred.id
|
const editable = id !== Constants.categories.all.id && id !== Constants.categories.starred.id
|
||||||
if (!category) return <Loader />
|
if (!category) return <Loader />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Code, Container, Group, List, Title } from "@mantine/core"
|
import { Anchor, Box, Code, Container, Group, List, Title } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { openConfirmModal } from "@mantine/modals"
|
import { openConfirmModal } from "@mantine/modals"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { useViewportSize } from "@mantine/hooks"
|
import { useViewportSize } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { ActionIcon, AppShell, Box, Center, Group, ScrollArea, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, AppShell, Box, Center, Group, ScrollArea, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
|
import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
|
||||||
@@ -18,7 +18,7 @@ import { useBrowserExtension } from "hooks/useBrowserExtension"
|
|||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { useWebSocket } from "hooks/useWebSocket"
|
import { useWebSocket } from "hooks/useWebSocket"
|
||||||
import { LoadingPage } from "pages/LoadingPage"
|
import { LoadingPage } from "pages/LoadingPage"
|
||||||
import { type ReactNode, Suspense, useEffect, useRef } from "react"
|
import { type ReactNode, type RefObject, Suspense, useEffect, useRef } from "react"
|
||||||
import Draggable from "react-draggable"
|
import Draggable from "react-draggable"
|
||||||
import { TbMenu2, TbPlus, TbX } from "react-icons/tb"
|
import { TbMenu2, TbPlus, TbX } from "react-icons/tb"
|
||||||
import { Outlet } from "react-router-dom"
|
import { Outlet } from "react-router-dom"
|
||||||
@@ -185,7 +185,7 @@ export default function Layout(props: LayoutProps) {
|
|||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
<OnDesktop>
|
<OnDesktop>
|
||||||
<Draggable
|
<Draggable
|
||||||
nodeRef={draggableSeparator}
|
nodeRef={draggableSeparator as RefObject<HTMLElement>}
|
||||||
axis="x"
|
axis="x"
|
||||||
defaultPosition={{
|
defaultPosition={{
|
||||||
x: sidebarWidth,
|
x: sidebarWidth,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Container, Tabs } from "@mantine/core"
|
import { Container, Tabs } from "@mantine/core"
|
||||||
import { CustomCodeSettings } from "components/settings/CustomCodeSettings"
|
import { CustomCodeSettings } from "components/settings/CustomCodeSettings"
|
||||||
import { DisplaySettings } from "components/settings/DisplaySettings"
|
import { DisplaySettings } from "components/settings/DisplaySettings"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
|
|
||||||
import { Anchor, Box, Button, Container, Group, Input, Stack, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Container, Group, Input, Stack, Title } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, msg } from "@lingui/macro"
|
import { msg } from "@lingui/core/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ export default defineConfig(() => ({
|
|||||||
plugins: [
|
plugins: [
|
||||||
react({
|
react({
|
||||||
babel: {
|
babel: {
|
||||||
// babel-macro is needed for lingui
|
plugins: ["@lingui/babel-plugin-lingui-macro"],
|
||||||
plugins: ["macros"],
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
lingui(),
|
lingui(),
|
||||||
|
|||||||
@@ -1,690 +0,0 @@
|
|||||||
:summaryTableId: commafeed-server_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|[.header-title]##Configuration property##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-hide-from-web-crawlers]] [.property-path]##link:#commafeed-server_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-server_commafeed-image-proxy-enabled]] [.property-path]##link:#commafeed-server_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-server_commafeed-password-recovery-enabled]] [.property-path]##link:#commafeed-server_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-server_commafeed-announcement]] [.property-path]##link:#commafeed-server_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-server_commafeed-google-analytics-tracking-code]] [.property-path]##link:#commafeed-server_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-server_commafeed-google-auth-key]] [.property-path]##link:#commafeed-server_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
|
|
||||||
|
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-http-client]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-http-client[HTTP client configuration]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-user-agent]] [.property-path]##link:#commafeed-server_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-server_commafeed-http-client-connect-timeout]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`5S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-ssl-handshake-timeout]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`5S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-socket-timeout]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`10S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-response-timeout]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`10S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-connection-time-to-live]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`30S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-idle-connections-eviction-interval]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`1M`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-max-response-size]] [.property-path]##link:#commafeed-server_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-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|
|
||||||
|`5M`
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-http-client-cache]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-http-client-cache[HTTP client cache configuration]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-cache-enabled]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-enabled[`commafeed.http-client.cache.enabled`]##
|
|
||||||
|
|
||||||
[.description]
|
|
||||||
--
|
|
||||||
Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the first time or when clicking "fetch all my feeds now").
|
|
||||||
|
|
||||||
|
|
||||||
ifdef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_ENABLED+++[]
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
ifndef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_ENABLED+++`
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
--
|
|
||||||
|boolean
|
|
||||||
|`true`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-cache-maximum-memory-size]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-maximum-memory-size[`commafeed.http-client.cache.maximum-memory-size`]##
|
|
||||||
|
|
||||||
[.description]
|
|
||||||
--
|
|
||||||
Maximum amount of memory the cache can use.
|
|
||||||
|
|
||||||
|
|
||||||
ifdef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_MAXIMUM_MEMORY_SIZE+++[]
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
ifndef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_MAXIMUM_MEMORY_SIZE+++`
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
--
|
|
||||||
|MemorySize link:#memory-size-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|
|
||||||
|`10M`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-http-client-cache-expiration]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-expiration[`commafeed.http-client.cache.expiration`]##
|
|
||||||
|
|
||||||
[.description]
|
|
||||||
--
|
|
||||||
Duration after which an entry is removed from the cache.
|
|
||||||
|
|
||||||
|
|
||||||
ifdef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_EXPIRATION+++[]
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
ifndef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_EXPIRATION+++`
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
--
|
|
||||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`1M`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-feed-refresh]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-feed-refresh[Feed refresh engine settings]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-interval]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`5M`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-interval-empirical]] [.property-path]##link:#commafeed-server_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-server_commafeed-feed-refresh-http-threads]] [.property-path]##link:#commafeed-server_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[]
|
|
||||||
--
|
|
||||||
|int
|
|
||||||
|`3`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-database-threads]] [.property-path]##link:#commafeed-server_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[]
|
|
||||||
--
|
|
||||||
|int
|
|
||||||
|`1`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-user-inactivity-period]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`0S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`500MS`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration[`commafeed.feed-refresh.force-refresh-cooldown-duration`]##
|
|
||||||
|
|
||||||
[.description]
|
|
||||||
--
|
|
||||||
Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
|
|
||||||
|
|
||||||
|
|
||||||
ifdef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++[]
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
ifndef::add-copy-button-to-env-var[]
|
|
||||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++`
|
|
||||||
endif::add-copy-button-to-env-var[]
|
|
||||||
--
|
|
||||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`0S`
|
|
||||||
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-database]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-database[Database settings]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-database-query-timeout]] [.property-path]##link:#commafeed-server_commafeed-database-query-timeout[`commafeed.database.query-timeout`]##
|
|
||||||
|
|
||||||
[.description]
|
|
||||||
--
|
|
||||||
Timeout applied to all database queries. 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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`0S`
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-database-cleanup]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-database-cleanup[Database cleanup settings]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-database-cleanup-entries-max-age]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`365D`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-database-cleanup-statuses-max-age]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`0S`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-database-cleanup-max-feed-capacity]] [.property-path]##link:#commafeed-server_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-server_commafeed-database-cleanup-max-feeds-per-user]] [.property-path]##link:#commafeed-server_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-server_commafeed-database-cleanup-batch-size]] [.property-path]##link:#commafeed-server_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[]
|
|
||||||
--
|
|
||||||
|int
|
|
||||||
|`100`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-users]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-users[Users settings]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-users-allow-registrations]] [.property-path]##link:#commafeed-server_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-server_commafeed-users-strict-password-policy]] [.property-path]##link:#commafeed-server_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-server_commafeed-users-create-demo-account]] [.property-path]##link:#commafeed-server_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`
|
|
||||||
|
|
||||||
|
|
||||||
h|[[commafeed-server_section_commafeed-websocket]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-websocket[Websocket settings]##
|
|
||||||
h|Type
|
|
||||||
h|Default
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-websocket-enabled]] [.property-path]##link:#commafeed-server_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-server_commafeed-websocket-ping-interval]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
|
||||||
|`15M`
|
|
||||||
|
|
||||||
a| [[commafeed-server_commafeed-websocket-tree-reload-interval]] [.property-path]##link:#commafeed-server_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/en/java/javase/17/docs/api/java.base/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-commafeed-server_commafeed]
|
|
||||||
.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[]
|
|
||||||
ifndef::no-memory-size-note[]
|
|
||||||
[NOTE]
|
|
||||||
[id=memory-size-note-anchor-commafeed-server_commafeed]
|
|
||||||
.About the MemorySize format
|
|
||||||
====
|
|
||||||
A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.
|
|
||||||
|
|
||||||
If no suffix is given, assume bytes.
|
|
||||||
====
|
|
||||||
ifndef::no-memory-size-note[]
|
|
||||||
|
|
||||||
:!summaryTableId:
|
|
||||||
894
commafeed-server/doc/commafeed.md
Normal file
894
commafeed-server/doc/commafeed.md
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
🔒: Configuration property fixed at build time - All other configuration properties are overridable at runtime
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left">Configuration property</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Default</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.hide-from-web-crawlers`
|
||||||
|
|
||||||
|
Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HIDE_FROM_WEB_CRAWLERS`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.image-proxy-enabled`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_IMAGE_PROXY_ENABLED`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`false`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.password-recovery-enabled`
|
||||||
|
|
||||||
|
Enable password recovery via email.
|
||||||
|
|
||||||
|
Quarkus mailer will need to be configured.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_PASSWORD_RECOVERY_ENABLED`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`false`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.announcement`
|
||||||
|
|
||||||
|
Message displayed in a notification at the bottom of the page.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_ANNOUNCEMENT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
string
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.google-analytics-tracking-code`
|
||||||
|
|
||||||
|
Google Analytics tracking code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
string
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.google-auth-key`
|
||||||
|
|
||||||
|
Google Auth key for fetching Youtube channel favicons.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_GOOGLE_AUTH_KEY`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
string
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
HTTP client configuration
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.user-agent`
|
||||||
|
|
||||||
|
User-Agent string that will be used by the http client, leave empty for the default one.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_USER_AGENT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
string
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.connect-timeout`
|
||||||
|
|
||||||
|
Time to wait for a connection to be established.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`5S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.ssl-handshake-timeout`
|
||||||
|
|
||||||
|
Time to wait for SSL handshake to complete.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`5S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.socket-timeout`
|
||||||
|
|
||||||
|
Time to wait between two packets before timeout.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`10S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.response-timeout`
|
||||||
|
|
||||||
|
Time to wait for the full response to be received.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`10S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.connection-time-to-live`
|
||||||
|
|
||||||
|
Time to live for a connection in the pool.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`30S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.idle-connections-eviction-interval`
|
||||||
|
|
||||||
|
Time between eviction runs for idle connections.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`1M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.max-response-size`
|
||||||
|
|
||||||
|
If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
MemorySize [🛈](#memory-size-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`5M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.block-local-addresses`
|
||||||
|
|
||||||
|
Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
|
||||||
|
resources.
|
||||||
|
|
||||||
|
You may want to disable this if you subscribe to feeds that are only available on your local network and you trust all users of
|
||||||
|
your CommaFeed instance.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_BLOCK_LOCAL_ADDRESSES`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
HTTP client cache configuration
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.cache.enabled`
|
||||||
|
|
||||||
|
Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the
|
||||||
|
first time or when clicking "fetch all my feeds now").
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_CACHE_ENABLED`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.cache.maximum-memory-size`
|
||||||
|
|
||||||
|
Maximum amount of memory the cache can use.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_CACHE_MAXIMUM_MEMORY_SIZE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
MemorySize [🛈](#memory-size-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`10M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.http-client.cache.expiration`
|
||||||
|
|
||||||
|
Duration after which an entry is removed from the cache.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_HTTP_CLIENT_CACHE_EXPIRATION`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`1M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Feed refresh engine settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.interval`
|
||||||
|
|
||||||
|
Default amount of time CommaFeed will wait before refreshing a feed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`5M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.max-interval`
|
||||||
|
|
||||||
|
Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an upper bound when:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>an error occurs while refreshing a feed and we're backing off exponentially</li>
|
||||||
|
<li>we receive a Cache-Control header from the feed</li>
|
||||||
|
<li>we receive a Retry-After header from the feed</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_MAX_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`4H`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.interval-empirical`
|
||||||
|
|
||||||
|
If enabled, 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 sometimes between the default refresh interval
|
||||||
|
(`commafeed.feed-refresh.interval`) and the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
|
||||||
|
|
||||||
|
See <code>FeedRefreshIntervalCalculator</code> for details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.http-threads`
|
||||||
|
|
||||||
|
Amount of http threads used to fetch feeds.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_HTTP_THREADS`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`3`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.database-threads`
|
||||||
|
|
||||||
|
Amount of threads used to insert new entries in the database.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_DATABASE_THREADS`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`1`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.user-inactivity-period`
|
||||||
|
|
||||||
|
Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`0S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.filtering-expression-evaluation-timeout`
|
||||||
|
|
||||||
|
Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`500MS`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.force-refresh-cooldown-duration`
|
||||||
|
|
||||||
|
Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`0S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Feed refresh engine error handling settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.errors.retries-before-backoff`
|
||||||
|
|
||||||
|
Number of retries before backoff is applied.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_ERRORS_RETRIES_BEFORE_BACKOFF`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`3`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.feed-refresh.errors.backoff-interval`
|
||||||
|
|
||||||
|
Duration to wait before retrying after an error. Will be multiplied by the number of errors since the last successful fetch.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_FEED_REFRESH_ERRORS_BACKOFF_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`1H`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Database settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.query-timeout`
|
||||||
|
|
||||||
|
Timeout applied to all database queries.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_QUERY_TIMEOUT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`0S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Database cleanup settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.cleanup.entries-max-age`
|
||||||
|
|
||||||
|
Maximum age of feed entries in the database. Older entries will be deleted.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`365D`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.cleanup.statuses-max-age`
|
||||||
|
|
||||||
|
Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`0S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.cleanup.max-feed-capacity`
|
||||||
|
|
||||||
|
Maximum number of entries per feed to keep in the database.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`500`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.cleanup.max-feeds-per-user`
|
||||||
|
|
||||||
|
Limit the number of feeds a user can subscribe to.
|
||||||
|
|
||||||
|
0 to disable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`0`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.database.cleanup.batch-size`
|
||||||
|
|
||||||
|
Rows to delete per query while cleaning up old entries.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
int
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`100`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Users settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.users.allow-registrations`
|
||||||
|
|
||||||
|
Whether to let users create accounts for themselves.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_USERS_ALLOW_REGISTRATIONS`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`false`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.users.strict-password-policy`
|
||||||
|
|
||||||
|
Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_USERS_STRICT_PASSWORD_POLICY`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.users.create-demo-account`
|
||||||
|
|
||||||
|
Whether to create a demo account the first time the app starts.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_USERS_CREATE_DEMO_ACCOUNT`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`false`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" colspan="3">
|
||||||
|
Websocket settings
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.websocket.enabled`
|
||||||
|
|
||||||
|
Enable websocket connection so the server can notify web clients that there are new entries for feeds.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_WEBSOCKET_ENABLED`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
boolean
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`true`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.websocket.ping-interval`
|
||||||
|
|
||||||
|
Interval at which the client will send a ping message on the websocket to keep the connection alive.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_WEBSOCKET_PING_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`15M`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`commafeed.websocket.tree-reload-interval`
|
||||||
|
|
||||||
|
If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment variable: `COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL`</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) [🛈](#duration-note-anchor)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`30S`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a name="duration-note-anchor"></a>
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> ### About the Duration format
|
||||||
|
>
|
||||||
|
> To write duration values, use the standard `java.time.Duration` format.
|
||||||
|
> See the [Duration#parse()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)) 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`.
|
||||||
|
<a name="memory-size-note-anchor"></a>
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> ### About the MemorySize format
|
||||||
|
>
|
||||||
|
> A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.
|
||||||
|
>
|
||||||
|
> If no suffix is given, assume bytes.
|
||||||
@@ -6,16 +6,16 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-server</artifactId>
|
<artifactId>commafeed-server</artifactId>
|
||||||
<name>CommaFeed Server</name>
|
<name>CommaFeed Server</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<quarkus.version>3.15.1</quarkus.version>
|
<quarkus.version>3.18.3</quarkus.version>
|
||||||
<querydsl.version>6.8</querydsl.version>
|
<querydsl.version>6.10.1</querydsl.version>
|
||||||
<rome.version>2.1.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
<swagger.version>2.2.25</swagger.version>
|
<swagger.version>2.2.28</swagger.version>
|
||||||
|
|
||||||
<build.database>h2</build.database>
|
<build.database>h2</build.database>
|
||||||
</properties>
|
</properties>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-help-plugin</artifactId>
|
<artifactId>maven-help-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.5.1</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>initialize</phase>
|
<phase>initialize</phase>
|
||||||
@@ -104,11 +104,14 @@
|
|||||||
<extensions>true</extensions>
|
<extensions>true</extensions>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>default-generate-asciidoc</id>
|
<id>default-generate-config-doc</id>
|
||||||
<phase>process-test-resources</phase>
|
<phase>process-test-resources</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>generate-asciidoc</goal>
|
<goal>generate-config-doc</goal>
|
||||||
</goals>
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<format>markdown</format>
|
||||||
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
@@ -135,7 +138,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.5.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<systemPropertyVariables>
|
<systemPropertyVariables>
|
||||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
@@ -146,7 +149,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-failsafe-plugin</artifactId>
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.5.2</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -233,12 +236,12 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.6.0</version>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
<groupId>com.puppycrawl.tools</groupId>
|
||||||
<artifactId>checkstyle</artifactId>
|
<artifactId>checkstyle</artifactId>
|
||||||
<version>10.18.2</version>
|
<version>10.21.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<executions>
|
<executions>
|
||||||
@@ -267,7 +270,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.diffplug.spotless</groupId>
|
<groupId>com.diffplug.spotless</groupId>
|
||||||
<artifactId>spotless-maven-plugin</artifactId>
|
<artifactId>spotless-maven-plugin</artifactId>
|
||||||
<version>2.43.0</version>
|
<version>2.44.2</version>
|
||||||
<?m2e ignore?>
|
<?m2e ignore?>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
@@ -294,14 +297,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- compile-time processors -->
|
<!-- compile-time processors -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.34</version>
|
<version>1.18.36</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -358,7 +361,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.dropwizard.metrics</groupId>
|
<groupId>io.dropwizard.metrics</groupId>
|
||||||
<artifactId>metrics-json</artifactId>
|
<artifactId>metrics-json</artifactId>
|
||||||
<version>4.2.28</version>
|
<version>4.2.30</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.swagger.core.v3</groupId>
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
@@ -405,7 +408,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.passay</groupId>
|
<groupId>org.passay</groupId>
|
||||||
<artifactId>passay</artifactId>
|
<artifactId>passay</artifactId>
|
||||||
<version>1.6.5</version>
|
<version>1.6.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.rometools</groupId>
|
<groupId>com.rometools</groupId>
|
||||||
@@ -430,12 +433,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.18.1</version>
|
<version>1.18.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ibm.icu</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>icu4j</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>75.1</version>
|
<version>76.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
@@ -450,7 +453,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
<artifactId>httpclient5</artifactId>
|
<artifactId>httpclient5</artifactId>
|
||||||
<version>5.4</version>
|
<version>5.4.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- add brotli support for httpclient5 -->
|
<!-- add brotli support for httpclient5 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -461,7 +464,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.hakky54</groupId>
|
<groupId>io.github.hakky54</groupId>
|
||||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||||
<version>8.3.7</version>
|
<version>9.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- test dependencies -->
|
<!-- test dependencies -->
|
||||||
@@ -489,7 +492,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.playwright</groupId>
|
<groupId>com.microsoft.playwright</groupId>
|
||||||
<artifactId>playwright</artifactId>
|
<artifactId>playwright</artifactId>
|
||||||
<version>1.47.0</version>
|
<version>1.50.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM ibm-semeru-runtimes:open-21.0.4_7-jre
|
FROM ibm-semeru-runtimes:open-21.0.5_11-jre
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
RUN mkdir -p /commafeed/data
|
RUN mkdir -p /commafeed/data
|
||||||
VOLUME /commafeed/data
|
VOLUME /commafeed/data
|
||||||
|
|
||||||
COPY commafeed-server/target/quarkus-app/ /commafeed
|
COPY artifacts/extracted-jvm-package/quarkus-app/ /commafeed
|
||||||
WORKDIR /commafeed
|
WORKDIR /commafeed
|
||||||
|
|
||||||
CMD ["java", \
|
CMD ["java", \
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
FROM debian:12.7
|
FROM debian:12.9
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
RUN mkdir -p /commafeed/data
|
RUN mkdir -p /commafeed/data
|
||||||
VOLUME /commafeed/data
|
VOLUME /commafeed/data
|
||||||
|
|
||||||
COPY commafeed-server/target/commafeed-*-runner /commafeed/application
|
COPY artifacts/commafeed-*-${TARGETARCH}-runner /commafeed/application
|
||||||
WORKDIR /commafeed
|
WORKDIR /commafeed
|
||||||
|
|
||||||
CMD ["./application"]
|
CMD ["./application"]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ CommaFeed also supports:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/doc/commafeed.adoc) are
|
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/doc/commafeed.md) are
|
||||||
optional and have sensible default values.
|
optional and have sensible default values.
|
||||||
|
|
||||||
Settings are overrideable with environment variables. For instance, `commafeed.feed-refresh.interval-empirical` can be
|
Settings are overrideable with environment variables. For instance, `commafeed.feed-refresh.interval-empirical` can be
|
||||||
@@ -92,5 +92,4 @@ Tags are of the form `<version>-<database>[-jvm]` where:
|
|||||||
- `latest` (always points to the latest version)
|
- `latest` (always points to the latest version)
|
||||||
- `master` (always points to the latest git commit)
|
- `master` (always points to the latest git commit)
|
||||||
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
||||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports
|
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively.
|
||||||
the arm64 platform which is not yet supported by the native image.
|
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ public interface CommaFeedConfiguration {
|
|||||||
@WithDefault("5M")
|
@WithDefault("5M")
|
||||||
MemorySize maxResponseSize();
|
MemorySize maxResponseSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
|
||||||
|
* resources.
|
||||||
|
*
|
||||||
|
* You may want to disable this if you subscribe to feeds that are only available on your local network and you trust all users of
|
||||||
|
* your CommaFeed instance.
|
||||||
|
*/
|
||||||
|
@WithDefault("true")
|
||||||
|
boolean blockLocalAddresses();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP client cache configuration
|
* HTTP client cache configuration
|
||||||
*/
|
*/
|
||||||
@@ -168,20 +178,39 @@ public interface CommaFeedConfiguration {
|
|||||||
|
|
||||||
interface FeedRefresh {
|
interface FeedRefresh {
|
||||||
/**
|
/**
|
||||||
* Amount of time CommaFeed will wait before refreshing the same feed.
|
* Default amount of time CommaFeed will wait before refreshing a feed.
|
||||||
*/
|
*/
|
||||||
@WithDefault("5m")
|
@WithDefault("5m")
|
||||||
Duration interval();
|
Duration interval();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the
|
* Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an upper bound when:
|
||||||
* last entry was published. The interval will be somewhere between the default refresh interval and 24h.
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>an error occurs while refreshing a feed and we're backing off exponentially</li>
|
||||||
|
* <li>we receive a Cache-Control header from the feed</li>
|
||||||
|
* <li>we receive a Retry-After header from the feed</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@WithDefault("4h")
|
||||||
|
Duration maxInterval();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enabled, 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 sometimes between the default refresh interval
|
||||||
|
* (`commafeed.feed-refresh.interval`) and the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
|
||||||
*
|
*
|
||||||
* See {@link FeedRefreshIntervalCalculator} for details.
|
* See {@link FeedRefreshIntervalCalculator} for details.
|
||||||
*/
|
*/
|
||||||
@WithDefault("false")
|
@WithDefault("true")
|
||||||
boolean intervalEmpirical();
|
boolean intervalEmpirical();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed refresh engine error handling settings.
|
||||||
|
*/
|
||||||
|
@ConfigDocSection
|
||||||
|
FeedRefreshErrorHandling errors();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount of http threads used to fetch feeds.
|
* Amount of http threads used to fetch feeds.
|
||||||
*/
|
*/
|
||||||
@@ -217,6 +246,21 @@ public interface CommaFeedConfiguration {
|
|||||||
Duration forceRefreshCooldownDuration();
|
Duration forceRefreshCooldownDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FeedRefreshErrorHandling {
|
||||||
|
/**
|
||||||
|
* Number of retries before backoff is applied.
|
||||||
|
*/
|
||||||
|
@Min(0)
|
||||||
|
@WithDefault("3")
|
||||||
|
int retriesBeforeBackoff();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration to wait before retrying after an error. Will be multiplied by the number of errors since the last successful fetch.
|
||||||
|
*/
|
||||||
|
@WithDefault("1h")
|
||||||
|
Duration backoffInterval();
|
||||||
|
}
|
||||||
|
|
||||||
interface Database {
|
interface Database {
|
||||||
/**
|
/**
|
||||||
* Timeout applied to all database queries.
|
* Timeout applied to all database queries.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.commafeed;
|
package com.commafeed;
|
||||||
|
|
||||||
|
import java.time.InstantSource;
|
||||||
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
|
||||||
import jakarta.enterprise.inject.Produces;
|
import jakarta.enterprise.inject.Produces;
|
||||||
@@ -8,9 +10,16 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class CommaFeedProducers {
|
public class CommaFeedProducers {
|
||||||
|
|
||||||
|
@Produces
|
||||||
|
@Singleton
|
||||||
|
public InstantSource instantSource() {
|
||||||
|
return InstantSource.system();
|
||||||
|
}
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
@Singleton
|
@Singleton
|
||||||
public MetricRegistry metricRegistry() {
|
public MetricRegistry metricRegistry() {
|
||||||
return new MetricRegistry();
|
return new MetricRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ package com.commafeed.backend;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.InstantSource;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hc.client5.http.DnsResolver;
|
||||||
|
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||||
import org.apache.hc.client5.http.config.RequestConfig;
|
import org.apache.hc.client5.http.config.RequestConfig;
|
||||||
import org.apache.hc.client5.http.config.TlsConfig;
|
import org.apache.hc.client5.http.config.TlsConfig;
|
||||||
@@ -20,6 +27,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
|
|||||||
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
||||||
import org.apache.hc.client5.http.protocol.HttpClientContext;
|
import org.apache.hc.client5.http.protocol.HttpClientContext;
|
||||||
import org.apache.hc.client5.http.protocol.RedirectLocations;
|
import org.apache.hc.client5.http.protocol.RedirectLocations;
|
||||||
|
import org.apache.hc.client5.http.utils.DateUtils;
|
||||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||||
import org.apache.hc.core5.http.Header;
|
import org.apache.hc.core5.http.Header;
|
||||||
import org.apache.hc.core5.http.HttpEntity;
|
import org.apache.hc.core5.http.HttpEntity;
|
||||||
@@ -29,6 +37,7 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
|
|||||||
import org.apache.hc.core5.http.message.BasicHeader;
|
import org.apache.hc.core5.http.message.BasicHeader;
|
||||||
import org.apache.hc.core5.util.TimeValue;
|
import org.apache.hc.core5.util.TimeValue;
|
||||||
import org.apache.hc.core5.util.Timeout;
|
import org.apache.hc.core5.util.Timeout;
|
||||||
|
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
|
||||||
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
@@ -41,9 +50,11 @@ import com.google.common.io.ByteStreams;
|
|||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
import jakarta.ws.rs.core.CacheControl;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.altindag.ssl.SSLFactory;
|
import nl.altindag.ssl.SSLFactory;
|
||||||
@@ -57,11 +68,14 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
|||||||
public class HttpGetter {
|
public class HttpGetter {
|
||||||
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
private final InstantSource instantSource;
|
||||||
private final CloseableHttpClient client;
|
private final CloseableHttpClient client;
|
||||||
private final Cache<HttpRequest, HttpResponse> cache;
|
private final Cache<HttpRequest, HttpResponse> cache;
|
||||||
|
private final DnsResolver dnsResolver = SystemDefaultDnsResolver.INSTANCE;
|
||||||
|
|
||||||
public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) {
|
public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, CommaFeedVersion version, MetricRegistry metrics) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.instantSource = instantSource;
|
||||||
|
|
||||||
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
|
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
|
||||||
String userAgent = config.httpClient()
|
String userAgent = config.httpClient()
|
||||||
@@ -81,11 +95,20 @@ public class HttpGetter {
|
|||||||
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> e.content != null ? e.content.length : 0).sum());
|
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> e.content != null ? e.content.length : 0).sum());
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResult get(String url) throws IOException, NotModifiedException {
|
public HttpResult get(String url)
|
||||||
|
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
|
||||||
return get(HttpRequest.builder(url).build());
|
return get(HttpRequest.builder(url).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResult get(HttpRequest request) throws IOException, NotModifiedException {
|
public HttpResult get(HttpRequest request)
|
||||||
|
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
|
||||||
|
URI uri = URI.create(request.getUrl());
|
||||||
|
ensureHttpScheme(uri.getScheme());
|
||||||
|
|
||||||
|
if (config.httpClient().blockLocalAddresses()) {
|
||||||
|
ensurePublicAddress(uri.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
final HttpResponse response;
|
final HttpResponse response;
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
response = invoke(request);
|
response = invoke(request);
|
||||||
@@ -102,9 +125,15 @@ public class HttpGetter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int code = response.getCode();
|
int code = response.getCode();
|
||||||
|
if (code == HttpStatus.SC_TOO_MANY_REQUESTS || code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.getRetryAfter() != null) {
|
||||||
|
throw new TooManyRequestsException(response.getRetryAfter());
|
||||||
|
}
|
||||||
|
|
||||||
if (code == HttpStatus.SC_NOT_MODIFIED) {
|
if (code == HttpStatus.SC_NOT_MODIFIED) {
|
||||||
throw new NotModifiedException("'304 - not modified' http code received");
|
throw new NotModifiedException("'304 - not modified' http code received");
|
||||||
} else if (code >= 300) {
|
}
|
||||||
|
|
||||||
|
if (code >= 300) {
|
||||||
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
|
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +147,35 @@ public class HttpGetter {
|
|||||||
throw new NotModifiedException("eTagHeader is the same");
|
throw new NotModifiedException("eTagHeader is the same");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Duration validFor = Optional.ofNullable(response.getCacheControl())
|
||||||
|
.filter(cc -> cc.getMaxAge() >= 0)
|
||||||
|
.map(cc -> Duration.ofSeconds(cc.getMaxAge()))
|
||||||
|
.orElse(Duration.ZERO);
|
||||||
|
|
||||||
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader,
|
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader,
|
||||||
response.getUrlAfterRedirect());
|
response.getUrlAfterRedirect(), validFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureHttpScheme(String scheme) throws SchemeNotAllowedException {
|
||||||
|
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||||
|
throw new SchemeNotAllowedException(scheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensurePublicAddress(String host) throws HostNotAllowedException, UnknownHostException {
|
||||||
|
if (host == null) {
|
||||||
|
throw new HostNotAllowedException(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
InetAddress[] addresses = dnsResolver.resolve(host);
|
||||||
|
if (Stream.of(addresses).anyMatch(this::isPrivateAddress)) {
|
||||||
|
throw new HostNotAllowedException(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPrivateAddress(InetAddress address) {
|
||||||
|
return address.isSiteLocalAddress() || address.isAnyLocalAddress() || address.isLinkLocalAddress() || address.isLoopbackAddress()
|
||||||
|
|| address.isMulticastAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpResponse invoke(HttpRequest request) throws IOException {
|
private HttpResponse invoke(HttpRequest request) throws IOException {
|
||||||
@@ -147,6 +203,18 @@ public class HttpGetter {
|
|||||||
.map(StringUtils::trimToNull)
|
.map(StringUtils::trimToNull)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
|
CacheControl cacheControl = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL))
|
||||||
|
.map(NameValuePair::getValue)
|
||||||
|
.map(StringUtils::trimToNull)
|
||||||
|
.map(HttpGetter::toCacheControl)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
Instant retryAfter = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
|
||||||
|
.map(NameValuePair::getValue)
|
||||||
|
.map(StringUtils::trimToNull)
|
||||||
|
.map(this::toInstant)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null);
|
String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null);
|
||||||
String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
|
String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
|
||||||
.map(RedirectLocations::getAll)
|
.map(RedirectLocations::getAll)
|
||||||
@@ -154,10 +222,31 @@ public class HttpGetter {
|
|||||||
.map(URI::toString)
|
.map(URI::toString)
|
||||||
.orElse(request.getUrl());
|
.orElse(request.getUrl());
|
||||||
|
|
||||||
return new HttpResponse(code, lastModifiedHeader, eTagHeader, content, contentType, urlAfterRedirect);
|
return new HttpResponse(code, lastModifiedHeader, eTagHeader, cacheControl, retryAfter, content, contentType, urlAfterRedirect);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CacheControl toCacheControl(String headerValue) {
|
||||||
|
try {
|
||||||
|
return CacheControlDelegate.INSTANCE.fromString(headerValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Invalid Cache-Control header: {}", headerValue);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant toInstant(String headerValue) {
|
||||||
|
if (headerValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNumeric(headerValue)) {
|
||||||
|
return instantSource.instant().plusSeconds(Long.parseLong(headerValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateUtils.parseStandardDate(headerValue);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
|
private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
|
||||||
if (entity.getContentLength() > maxBytes) {
|
if (entity.getContentLength() > maxBytes) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
@@ -177,7 +266,7 @@ public class HttpGetter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) {
|
private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) {
|
||||||
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
|
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
|
||||||
|
|
||||||
int poolSize = config.feedRefresh().httpThreads();
|
int poolSize = config.feedRefresh().httpThreads();
|
||||||
@@ -191,6 +280,7 @@ public class HttpGetter {
|
|||||||
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
|
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
|
||||||
.setMaxConnPerRoute(poolSize)
|
.setMaxConnPerRoute(poolSize)
|
||||||
.setMaxConnTotal(poolSize)
|
.setMaxConnTotal(poolSize)
|
||||||
|
.setDnsResolver(dnsResolver)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -227,6 +317,22 @@ public class HttpGetter {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class SchemeNotAllowedException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public SchemeNotAllowedException(String scheme) {
|
||||||
|
super("Scheme not allowed: " + scheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HostNotAllowedException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public HostNotAllowedException(String host) {
|
||||||
|
super("Host not allowed: " + host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public static class NotModifiedException extends Exception {
|
public static class NotModifiedException extends Exception {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -252,6 +358,14 @@ public class HttpGetter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class TooManyRequestsException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Instant retryAfter;
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public static class HttpResponseException extends IOException {
|
public static class HttpResponseException extends IOException {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -293,6 +407,8 @@ public class HttpGetter {
|
|||||||
int code;
|
int code;
|
||||||
String lastModifiedHeader;
|
String lastModifiedHeader;
|
||||||
String eTagHeader;
|
String eTagHeader;
|
||||||
|
CacheControl cacheControl;
|
||||||
|
Instant retryAfter;
|
||||||
byte[] content;
|
byte[] content;
|
||||||
String contentType;
|
String contentType;
|
||||||
String urlAfterRedirect;
|
String urlAfterRedirect;
|
||||||
@@ -305,6 +421,7 @@ public class HttpGetter {
|
|||||||
String lastModifiedSince;
|
String lastModifiedSince;
|
||||||
String eTag;
|
String eTag;
|
||||||
String urlAfterRedirect;
|
String urlAfterRedirect;
|
||||||
|
Duration validFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import org.apache.hc.core5.net.URIBuilder;
|
|||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.HttpGetter;
|
import com.commafeed.backend.HttpGetter;
|
||||||
|
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
|
||||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.SchemeNotAllowedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.fasterxml.jackson.core.JsonPointer;
|
import com.fasterxml.jackson.core.JsonPointer;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@@ -91,7 +94,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
return new Favicon(bytes, contentType);
|
return new Favicon(bytes, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] fetchForUser(String googleAuthKey, String userId) throws IOException, NotModifiedException {
|
private byte[] fetchForUser(String googleAuthKey, String userId)
|
||||||
|
throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
|
||||||
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
||||||
.queryParam("part", "snippet")
|
.queryParam("part", "snippet")
|
||||||
.queryParam("key", googleAuthKey)
|
.queryParam("key", googleAuthKey)
|
||||||
@@ -100,7 +104,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
return getter.get(uri.toString()).getContent();
|
return getter.get(uri.toString()).getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] fetchForChannel(String googleAuthKey, String channelId) throws IOException, NotModifiedException {
|
private byte[] fetchForChannel(String googleAuthKey, String channelId)
|
||||||
|
throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
|
||||||
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
||||||
.queryParam("part", "snippet")
|
.queryParam("part", "snippet")
|
||||||
.queryParam("key", googleAuthKey)
|
.queryParam("key", googleAuthKey)
|
||||||
@@ -109,7 +114,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
return getter.get(uri.toString()).getContent();
|
return getter.get(uri.toString()).getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId) throws IOException, NotModifiedException {
|
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
|
||||||
|
throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
|
||||||
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
|
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
|
||||||
.queryParam("part", "snippet")
|
.queryParam("part", "snippet")
|
||||||
.queryParam("key", googleAuthKey)
|
.queryParam("key", googleAuthKey)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.feed;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -9,9 +10,12 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
|
|
||||||
import com.commafeed.backend.Digests;
|
import com.commafeed.backend.Digests;
|
||||||
import com.commafeed.backend.HttpGetter;
|
import com.commafeed.backend.HttpGetter;
|
||||||
|
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
|
||||||
import com.commafeed.backend.HttpGetter.HttpRequest;
|
import com.commafeed.backend.HttpGetter.HttpRequest;
|
||||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.SchemeNotAllowedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||||
import com.commafeed.backend.feed.parser.FeedParser;
|
import com.commafeed.backend.feed.parser.FeedParser;
|
||||||
import com.commafeed.backend.feed.parser.FeedParserResult;
|
import com.commafeed.backend.feed.parser.FeedParserResult;
|
||||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||||
@@ -39,7 +43,8 @@ public class FeedFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
|
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
|
||||||
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException,
|
||||||
|
TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
|
||||||
log.debug("Fetching feed {}", feedUrl);
|
log.debug("Fetching feed {}", feedUrl);
|
||||||
|
|
||||||
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
|
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
|
||||||
@@ -87,7 +92,8 @@ public class FeedFetcher {
|
|||||||
etagHeaderValueChanged ? result.getETag() : null);
|
etagHeaderValueChanged ? result.getETag() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash);
|
return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash,
|
||||||
|
result.getValidFor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractFeedUrl(List<FeedURLProvider> urlProviders, String url, String urlContent) {
|
private static String extractFeedUrl(List<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||||
@@ -102,7 +108,7 @@ public class FeedFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader,
|
public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader,
|
||||||
String contentHash) {
|
String contentHash, Duration validFor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,77 +2,83 @@ package com.commafeed.backend.feed;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.InstantSource;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
|
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedRefreshIntervalCalculator {
|
public class FeedRefreshIntervalCalculator {
|
||||||
|
|
||||||
private final Duration refreshInterval;
|
private final Duration interval;
|
||||||
private final boolean empiricalInterval;
|
private final Duration maxInterval;
|
||||||
|
private final boolean empirical;
|
||||||
|
private final FeedRefreshErrorHandling errorHandling;
|
||||||
|
private final InstantSource instantSource;
|
||||||
|
|
||||||
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {
|
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config, InstantSource instantSource) {
|
||||||
this.refreshInterval = config.feedRefresh().interval();
|
this.interval = config.feedRefresh().interval();
|
||||||
this.empiricalInterval = config.feedRefresh().intervalEmpirical();
|
this.maxInterval = config.feedRefresh().maxInterval();
|
||||||
|
this.empirical = config.feedRefresh().intervalEmpirical();
|
||||||
|
this.errorHandling = config.feedRefresh().errors();
|
||||||
|
this.instantSource = instantSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) {
|
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval, Duration validFor) {
|
||||||
Instant defaultRefreshInterval = getDefaultRefreshInterval();
|
Instant instant = empirical ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
|
||||||
return empiricalInterval ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval, defaultRefreshInterval)
|
: instantSource.instant().plus(interval);
|
||||||
: defaultRefreshInterval;
|
return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant onFeedNotModified(Instant publishedDate, Long averageEntryInterval) {
|
public Instant onFeedNotModified(Instant publishedDate, Long averageEntryInterval) {
|
||||||
return onFetchSuccess(publishedDate, averageEntryInterval);
|
return onFetchSuccess(publishedDate, averageEntryInterval, Duration.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant onTooManyRequests(Instant retryAfter, int errorCount) {
|
||||||
|
return constrainToBounds(ObjectUtils.max(retryAfter, onFetchError(errorCount)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant onFetchError(int errorCount) {
|
public Instant onFetchError(int errorCount) {
|
||||||
int retriesBeforeDisable = 3;
|
if (errorCount < errorHandling.retriesBeforeBackoff()) {
|
||||||
if (errorCount < retriesBeforeDisable || !empiricalInterval) {
|
return constrainToBounds(instantSource.instant().plus(interval));
|
||||||
return getDefaultRefreshInterval();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int disabledHours = Math.min(24 * 7, errorCount - retriesBeforeDisable + 1);
|
Duration retryInterval = errorHandling.backoffInterval().multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
|
||||||
return Instant.now().plus(Duration.ofHours(disabledHours));
|
return constrainToBounds(instantSource.instant().plus(retryInterval));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Instant getDefaultRefreshInterval() {
|
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval) {
|
||||||
return Instant.now().plus(refreshInterval);
|
Instant now = instantSource.instant();
|
||||||
}
|
|
||||||
|
|
||||||
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
|
|
||||||
Instant now = Instant.now();
|
|
||||||
|
|
||||||
if (publishedDate == null) {
|
if (publishedDate == null) {
|
||||||
// feed with no entries, recheck in 24 hours
|
return now.plus(maxInterval);
|
||||||
return now.plus(Duration.ofHours(24));
|
}
|
||||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 30) {
|
|
||||||
// older than a month, recheck in 24 hours
|
long daysSinceLastPublication = ChronoUnit.DAYS.between(publishedDate, now);
|
||||||
return now.plus(Duration.ofHours(24));
|
if (daysSinceLastPublication >= 30) {
|
||||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 14) {
|
return now.plus(maxInterval);
|
||||||
// older than two weeks, recheck in 12 hours
|
} else if (daysSinceLastPublication >= 14) {
|
||||||
return now.plus(Duration.ofHours(12));
|
return now.plus(maxInterval.dividedBy(2));
|
||||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 7) {
|
} else if (daysSinceLastPublication >= 7) {
|
||||||
// older than a week, recheck in 6 hours
|
return now.plus(maxInterval.dividedBy(4));
|
||||||
return now.plus(Duration.ofHours(6));
|
|
||||||
} else if (averageEntryInterval != null) {
|
} else if (averageEntryInterval != null) {
|
||||||
// use average time between entries to decide when to refresh next, divided by factor
|
// use average time between entries to decide when to refresh next, divided by factor
|
||||||
int factor = 2;
|
int factor = 2;
|
||||||
|
long millis = Longs.constrainToRange(averageEntryInterval / factor, interval.toMillis(), maxInterval.dividedBy(4).toMillis());
|
||||||
// not more than 6 hours
|
return now.plusMillis(millis);
|
||||||
long date = Math.min(now.plus(Duration.ofHours(6)).toEpochMilli(), now.toEpochMilli() + averageEntryInterval / factor);
|
|
||||||
|
|
||||||
// not less than default refresh interval
|
|
||||||
date = Math.max(defaultRefreshInterval.toEpochMilli(), date);
|
|
||||||
|
|
||||||
return Instant.ofEpochMilli(date);
|
|
||||||
} else {
|
} else {
|
||||||
// unknown case, recheck in 24 hours
|
// unknown case
|
||||||
return now.plus(Duration.ofHours(24));
|
return now.plus(maxInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Instant constrainToBounds(Instant instant) {
|
||||||
|
return ObjectUtils.max(ObjectUtils.min(instant, instantSource.instant().plus(maxInterval)), instantSource.instant().plus(interval));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.codahale.metrics.Meter;
|
|||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||||
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
@@ -76,8 +77,8 @@ public class FeedRefreshWorker {
|
|||||||
|
|
||||||
feed.setErrorCount(0);
|
feed.setErrorCount(0);
|
||||||
feed.setMessage(null);
|
feed.setMessage(null);
|
||||||
feed.setDisabledUntil(
|
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(),
|
||||||
refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(), result.feed().averageEntryInterval()));
|
result.feed().averageEntryInterval(), result.validFor()));
|
||||||
|
|
||||||
return new FeedRefreshWorkerResult(feed, entries);
|
return new FeedRefreshWorkerResult(feed, entries);
|
||||||
} catch (NotModifiedException e) {
|
} catch (NotModifiedException e) {
|
||||||
@@ -95,6 +96,14 @@ public class FeedRefreshWorker {
|
|||||||
feed.setEtagHeader(e.getNewEtagHeader());
|
feed.setEtagHeader(e.getNewEtagHeader());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||||
|
} catch (TooManyRequestsException e) {
|
||||||
|
log.debug("Too many requests : {}", feed.getUrl());
|
||||||
|
|
||||||
|
feed.setErrorCount(feed.getErrorCount() + 1);
|
||||||
|
feed.setMessage("Server indicated that we are sending too many requests");
|
||||||
|
feed.setDisabledUntil(refreshIntervalCalculator.onTooManyRequests(e.getRetryAfter(), feed.getErrorCount()));
|
||||||
|
|
||||||
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("unable to refresh feed {}", feed.getUrl(), e);
|
log.debug("unable to refresh feed {}", feed.getUrl(), e);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.commafeed.backend.feed.parser;
|
package com.commafeed.backend.feed.parser;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.ahocorasick.trie.Emit;
|
import org.ahocorasick.trie.Emit;
|
||||||
import org.ahocorasick.trie.Trie;
|
import org.ahocorasick.trie.Trie;
|
||||||
@@ -11,6 +12,8 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
class FeedCleaner {
|
class FeedCleaner {
|
||||||
|
|
||||||
|
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
public String trimInvalidXmlCharacters(String xml) {
|
public String trimInvalidXmlCharacters(String xml) {
|
||||||
if (StringUtils.isBlank(xml)) {
|
if (StringUtils.isBlank(xml)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -60,4 +63,8 @@ class FeedCleaner {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String removeDoctypeDeclarations(String xml) {
|
||||||
|
return DOCTYPE_PATTERN.matcher(xml).replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public class FeedParser {
|
|||||||
throw new FeedException("Input string is null for url " + feedUrl);
|
throw new FeedException("Input string is null for url " + feedUrl);
|
||||||
}
|
}
|
||||||
xmlString = feedCleaner.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
xmlString = feedCleaner.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
||||||
|
xmlString = feedCleaner.removeDoctypeDeclarations(xmlString);
|
||||||
|
|
||||||
InputSource source = new InputSource(new StringReader(xmlString));
|
InputSource source = new InputSource(new StringReader(xmlString));
|
||||||
SyndFeed feed = new SyndFeedInput().build(source);
|
SyndFeed feed = new SyndFeedInput().build(source);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.commafeed.backend.urlprovider;
|
package com.commafeed.backend.urlprovider;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
@@ -14,12 +13,16 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class YoutubeFeedURLProvider implements FeedURLProvider {
|
public class YoutubeFeedURLProvider implements FeedURLProvider {
|
||||||
|
|
||||||
private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE);
|
private static final String PREFIX = "https://www.youtube.com/channel/";
|
||||||
|
private static final String REPLACEMENT_PREFIX = "https://www.youtube.com/feeds/videos.xml?channel_id=";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String get(String url, String urlContent) {
|
public String get(String url, String urlContent) {
|
||||||
Matcher matcher = REGEXP.matcher(url);
|
if (!StringUtils.startsWithIgnoreCase(url, PREFIX)) {
|
||||||
return matcher.find() ? matcher.group(1) + "/feeds/videos.xml?channel_id=" + matcher.group(2) : null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REPLACEMENT_PREFIX + url.substring(PREFIX.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ public class UserREST {
|
|||||||
return Response.status(Status.UNAUTHORIZED).entity("token expired.").build();
|
return Response.status(Status.UNAUTHORIZED).entity("token expired.").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
String passwd = RandomStringUtils.randomAlphanumeric(10);
|
String passwd = RandomStringUtils.secure().nextAlphanumeric(10);
|
||||||
byte[] encryptedPassword = encryptionService.getEncryptedPassword(passwd, user.getSalt());
|
byte[] encryptedPassword = encryptionService.getEncryptedPassword(passwd, user.getSalt());
|
||||||
user.setPassword(encryptedPassword);
|
user.setPassword(encryptedPassword);
|
||||||
if (StringUtils.isNotBlank(user.getApiKey())) {
|
if (StringUtils.isNotBlank(user.getApiKey())) {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.commafeed.security.mechanism;
|
|
||||||
|
|
||||||
import io.quarkus.security.identity.IdentityProviderManager;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import io.quarkus.vertx.http.runtime.HttpConfiguration;
|
|
||||||
import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism;
|
|
||||||
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import io.vertx.core.http.Cookie;
|
|
||||||
import io.vertx.core.http.impl.ServerCookie;
|
|
||||||
import io.vertx.ext.web.RoutingContext;
|
|
||||||
import jakarta.annotation.Priority;
|
|
||||||
import jakarta.inject.Singleton;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.experimental.Delegate;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HttpAuthenticationMechanism that wraps FormAuthenticationMechanism and sets a Max-Age on the cookie because it has no value by default.
|
|
||||||
*
|
|
||||||
* This is a workaround for https://github.com/quarkusio/quarkus/issues/42463
|
|
||||||
*/
|
|
||||||
@Priority(1)
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Singleton
|
|
||||||
@Slf4j
|
|
||||||
public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticationMechanism {
|
|
||||||
|
|
||||||
@Delegate
|
|
||||||
private final FormAuthenticationMechanism delegate;
|
|
||||||
private final HttpConfiguration config;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
|
||||||
context.addHeadersEndHandler(v -> {
|
|
||||||
Cookie cookie = context.request().getCookie(config.auth.form.cookieName);
|
|
||||||
if (cookie instanceof ServerCookie sc && sc.isChanged()) {
|
|
||||||
cookie.setMaxAge(config.auth.form.timeout.toSeconds());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return delegate.authenticate(context, identityProviderManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@ quarkus.http.auth.basic=true
|
|||||||
quarkus.http.auth.form.enabled=true
|
quarkus.http.auth.form.enabled=true
|
||||||
quarkus.http.auth.form.http-only-cookie=true
|
quarkus.http.auth.form.http-only-cookie=true
|
||||||
quarkus.http.auth.form.timeout=P30d
|
quarkus.http.auth.form.timeout=P30d
|
||||||
|
quarkus.http.auth.form.cookie-max-age=P30d
|
||||||
quarkus.http.auth.form.landing-page=
|
quarkus.http.auth.form.landing-page=
|
||||||
quarkus.http.auth.form.login-page=
|
quarkus.http.auth.form.login-page=
|
||||||
quarkus.http.auth.form.error-page=
|
quarkus.http.auth.form.error-page=
|
||||||
@@ -49,6 +50,8 @@ quarkus.native.add-all-charsets=true
|
|||||||
%test.commafeed.users.allow-registrations=true
|
%test.commafeed.users.allow-registrations=true
|
||||||
%test.commafeed.password-recovery-enabled=true
|
%test.commafeed.password-recovery-enabled=true
|
||||||
%test.commafeed.http-client.cache.enabled=false
|
%test.commafeed.http-client.cache.enabled=false
|
||||||
|
%test.commafeed.http-client.block-local-addresses=false
|
||||||
|
%test.commafeed.database.cleanup.entries-max-age=0
|
||||||
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
|
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import org.junit.jupiter.api.Test;
|
|||||||
class CommaFeedConfigurationTest {
|
class CommaFeedConfigurationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void verifyAsciiDocIsUpToDate() throws IOException {
|
void verifyMarkdownDocIsUpToDate() throws IOException {
|
||||||
String versionedDocumentationFile = FileUtils.readFileToString(new File("doc/commafeed.adoc"), StandardCharsets.UTF_8);
|
String versionedDocumentationFile = FileUtils.readFileToString(new File("doc/commafeed.md"), StandardCharsets.UTF_8);
|
||||||
String generatedDocumentationFile = FileUtils
|
String generatedDocumentationFile = FileUtils.readFileToString(new File("target/quarkus-generated-doc/config/commafeed-server.md"),
|
||||||
.readFileToString(new File("target/quarkus-generated-doc/config/commafeed-server.adoc"), StandardCharsets.UTF_8);
|
StandardCharsets.UTF_8);
|
||||||
|
|
||||||
Assertions.assertLinesMatch(versionedDocumentationFile.lines(), generatedDocumentationFile.lines());
|
Assertions.assertLinesMatch(versionedDocumentationFile.lines(), generatedDocumentationFile.lines());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.OutputStream;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -39,6 +40,7 @@ import com.commafeed.CommaFeedVersion;
|
|||||||
import com.commafeed.backend.HttpGetter.HttpResponseException;
|
import com.commafeed.backend.HttpGetter.HttpResponseException;
|
||||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
|
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
import io.quarkus.runtime.configuration.MemorySize;
|
import io.quarkus.runtime.configuration.MemorySize;
|
||||||
@@ -46,9 +48,12 @@ import io.quarkus.runtime.configuration.MemorySize;
|
|||||||
@ExtendWith(MockServerExtension.class)
|
@ExtendWith(MockServerExtension.class)
|
||||||
class HttpGetterTest {
|
class HttpGetterTest {
|
||||||
|
|
||||||
|
private static final Instant NOW = Instant.now();
|
||||||
|
|
||||||
private MockServerClient mockServerClient;
|
private MockServerClient mockServerClient;
|
||||||
private String feedUrl;
|
private String feedUrl;
|
||||||
private byte[] feedContent;
|
private byte[] feedContent;
|
||||||
|
|
||||||
private CommaFeedConfiguration config;
|
private CommaFeedConfiguration config;
|
||||||
|
|
||||||
private HttpGetter getter;
|
private HttpGetter getter;
|
||||||
@@ -73,7 +78,7 @@ class HttpGetterTest {
|
|||||||
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
|
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
|
||||||
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
|
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
|
||||||
|
|
||||||
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -93,16 +98,52 @@ class HttpGetterTest {
|
|||||||
.withBody(feedContent)
|
.withBody(feedContent)
|
||||||
.withContentType(MediaType.APPLICATION_ATOM_XML)
|
.withContentType(MediaType.APPLICATION_ATOM_XML)
|
||||||
.withHeader(HttpHeaders.LAST_MODIFIED, "123456")
|
.withHeader(HttpHeaders.LAST_MODIFIED, "123456")
|
||||||
.withHeader(HttpHeaders.ETAG, "78910"));
|
.withHeader(HttpHeaders.ETAG, "78910")
|
||||||
|
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate")
|
||||||
|
.withHeader(HttpHeaders.RETRY_AFTER, "120"));
|
||||||
|
|
||||||
HttpResult result = getter.get(this.feedUrl);
|
HttpResult result = getter.get(this.feedUrl);
|
||||||
Assertions.assertArrayEquals(feedContent, result.getContent());
|
Assertions.assertArrayEquals(feedContent, result.getContent());
|
||||||
Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.getContentType());
|
Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.getContentType());
|
||||||
Assertions.assertEquals("123456", result.getLastModifiedSince());
|
Assertions.assertEquals("123456", result.getLastModifiedSince());
|
||||||
Assertions.assertEquals("78910", result.getETag());
|
Assertions.assertEquals("78910", result.getETag());
|
||||||
|
Assertions.assertEquals(Duration.ofSeconds(60), result.getValidFor());
|
||||||
Assertions.assertEquals(this.feedUrl, result.getUrlAfterRedirect());
|
Assertions.assertEquals(this.feedUrl, result.getUrlAfterRedirect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoreInvalidCacheControlValue() throws Exception {
|
||||||
|
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
|
||||||
|
.respond(HttpResponse.response()
|
||||||
|
.withBody(feedContent)
|
||||||
|
.withContentType(MediaType.APPLICATION_ATOM_XML)
|
||||||
|
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60; must-revalidate"));
|
||||||
|
|
||||||
|
HttpResult result = getter.get(this.feedUrl);
|
||||||
|
Assertions.assertEquals(Duration.ZERO, result.getValidFor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tooManyRequestsExceptionSeconds() {
|
||||||
|
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
|
||||||
|
.respond(
|
||||||
|
HttpResponse.response().withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS).withHeader(HttpHeaders.RETRY_AFTER, "120"));
|
||||||
|
|
||||||
|
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
|
||||||
|
Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tooManyRequestsExceptionDate() {
|
||||||
|
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
|
||||||
|
.respond(HttpResponse.response()
|
||||||
|
.withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS)
|
||||||
|
.withHeader(HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT"));
|
||||||
|
|
||||||
|
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
|
||||||
|
Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter());
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(
|
@ValueSource(
|
||||||
ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
|
ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
|
||||||
@@ -131,7 +172,7 @@ class HttpGetterTest {
|
|||||||
@Test
|
@Test
|
||||||
void dataTimeout() {
|
void dataTimeout() {
|
||||||
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
|
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
|
||||||
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
||||||
|
|
||||||
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
|
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
|
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
|
||||||
@@ -142,7 +183,7 @@ class HttpGetterTest {
|
|||||||
@Test
|
@Test
|
||||||
void connectTimeout() {
|
void connectTimeout() {
|
||||||
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
|
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
|
||||||
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
||||||
// try to connect to a non-routable address
|
// try to connect to a non-routable address
|
||||||
// https://stackoverflow.com/a/904609
|
// https://stackoverflow.com/a/904609
|
||||||
Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.get("http://10.255.255.1"));
|
Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.get("http://10.255.255.1"));
|
||||||
@@ -195,7 +236,7 @@ class HttpGetterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cacheSubsequentCalls() throws IOException, NotModifiedException {
|
void cacheSubsequentCalls() throws Exception {
|
||||||
AtomicInteger calls = new AtomicInteger();
|
AtomicInteger calls = new AtomicInteger();
|
||||||
|
|
||||||
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
|
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
|
||||||
@@ -261,17 +302,16 @@ class HttpGetterTest {
|
|||||||
class Compression {
|
class Compression {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void deflate() throws IOException, NotModifiedException {
|
void deflate() throws Exception {
|
||||||
supportsCompression("deflate", DeflaterOutputStream::new);
|
supportsCompression("deflate", DeflaterOutputStream::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void gzip() throws IOException, NotModifiedException {
|
void gzip() throws Exception {
|
||||||
supportsCompression("gzip", GZIPOutputStream::new);
|
supportsCompression("gzip", GZIPOutputStream::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction)
|
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction) throws Exception {
|
||||||
throws IOException, NotModifiedException {
|
|
||||||
String body = "my body";
|
String body = "my body";
|
||||||
|
|
||||||
HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
|
HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
|
||||||
@@ -299,4 +339,64 @@ class HttpGetterTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class SchemeNotAllowed {
|
||||||
|
@Test
|
||||||
|
void file() {
|
||||||
|
Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("file://localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ftp() {
|
||||||
|
Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("ftp://localhost"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class HostNotAllowed {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() {
|
||||||
|
Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true);
|
||||||
|
getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void localhost() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://localhost"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://127.0.0.1"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://2130706433"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zero() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://0.0.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void linkLocal() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://169.254.12.34"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://169.254.169.254"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multicast() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://224.2.3.4"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://239.255.255.254"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void privateIpv4Ranges() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://10.0.0.1"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://172.16.0.1"));
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://192.168.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void privateIpv6Ranges() {
|
||||||
|
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://fd12:3456:789a:1::1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ class FeedFetcherTest {
|
|||||||
String lastContentHash = Digests.sha1Hex(content);
|
String lastContentHash = Digests.sha1Hex(content);
|
||||||
|
|
||||||
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(url).lastModified(lastModified).eTag(etag).build()))
|
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(url).lastModified(lastModified).eTag(etag).build()))
|
||||||
.thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", null));
|
.thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", null, Duration.ZERO));
|
||||||
|
|
||||||
NotModifiedException e = Assertions.assertThrows(NotModifiedException.class,
|
NotModifiedException e = Assertions.assertThrows(NotModifiedException.class,
|
||||||
() -> fetcher.fetch(url, false, lastModified, etag, Instant.now(), lastContentHash));
|
() -> fetcher.fetch(url, false, lastModified, etag, Instant.now(), lastContentHash));
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.InstantSource;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
|
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class FeedRefreshIntervalCalculatorTest {
|
||||||
|
|
||||||
|
private static final Instant NOW = Instant.now();
|
||||||
|
private static final Duration DEFAULT_INTERVAL = Duration.ofHours(1);
|
||||||
|
private static final Duration MAX_INTERVAL = Duration.ofDays(1);
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InstantSource instantSource;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CommaFeedConfiguration config;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FeedRefreshErrorHandling errorHandling;
|
||||||
|
|
||||||
|
private FeedRefreshIntervalCalculator calculator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Mockito.when(instantSource.instant()).thenReturn(NOW);
|
||||||
|
Mockito.when(config.feedRefresh()).thenReturn(Mockito.mock(CommaFeedConfiguration.FeedRefresh.class));
|
||||||
|
Mockito.when(config.feedRefresh().interval()).thenReturn(DEFAULT_INTERVAL);
|
||||||
|
Mockito.when(config.feedRefresh().maxInterval()).thenReturn(MAX_INTERVAL);
|
||||||
|
Mockito.when(config.feedRefresh().errors()).thenReturn(errorHandling);
|
||||||
|
|
||||||
|
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FetchSuccess {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class EmpiricalDisabled {
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(longs = { 0, 1, 300, 86400000L })
|
||||||
|
void withoutValidFor(long averageEntryInterval) {
|
||||||
|
// averageEntryInterval is ignored when empirical is disabled
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), averageEntryInterval, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withValidForGreaterThanMaxInterval() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.plusDays(1));
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withValidForLowerThanMaxInterval() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.minusSeconds(1));
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL).minusSeconds(1), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class EmpiricalEnabled {
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
|
||||||
|
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withNullPublishedDate() {
|
||||||
|
Instant result = calculator.onFetchSuccess(null, 1L, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with31DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(31)), 1L, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with15DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(15)), 1L, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with8DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(8)), 1L, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FiveDaysOld {
|
||||||
|
@Test
|
||||||
|
void averageBetweenBounds() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis(),
|
||||||
|
Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void averageBelowMinimum() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 10L, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void averageAboveMaximum() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noAverage() {
|
||||||
|
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), null, Duration.ZERO);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FeedNotModified {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class EmpiricalDisabled {
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(longs = { 0, 1, 300, 86400000L })
|
||||||
|
void withoutValidFor(long averageEntryInterval) {
|
||||||
|
// averageEntryInterval is ignored when empirical is disabled
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), averageEntryInterval);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class EmpiricalEnabled {
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
|
||||||
|
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withNullPublishedDate() {
|
||||||
|
Instant result = calculator.onFeedNotModified(null, 1L);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with31DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(31)), 1L);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with15DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(15)), 1L);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void with8DaysOldPublishedDate() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(8)), 1L);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FiveDaysOld {
|
||||||
|
@Test
|
||||||
|
void averageBetweenBounds() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis());
|
||||||
|
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void averageBelowMinimum() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), 10L);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void averageAboveMaximum() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noAverage() {
|
||||||
|
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), null);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FetchError {
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lowErrorCount() {
|
||||||
|
Instant result = calculator.onFetchError(1);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void highErrorCount() {
|
||||||
|
Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1));
|
||||||
|
|
||||||
|
Instant result = calculator.onFetchError(5);
|
||||||
|
Assertions.assertEquals(NOW.plus(Duration.ofHours(3)), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void veryHighErrorCount() {
|
||||||
|
Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1));
|
||||||
|
|
||||||
|
Instant result = calculator.onFetchError(100000);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class TooManyRequests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withRetryAfterZero() {
|
||||||
|
Instant result = calculator.onTooManyRequests(NOW, 1);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withRetryAfterLowerThanInterval() {
|
||||||
|
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.minusSeconds(10));
|
||||||
|
Instant result = calculator.onTooManyRequests(retryAfter, 1);
|
||||||
|
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withRetryAfterBetweenBounds() {
|
||||||
|
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.plusSeconds(10));
|
||||||
|
Instant result = calculator.onTooManyRequests(retryAfter, 1);
|
||||||
|
Assertions.assertEquals(retryAfter, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withRetryAfterGreaterThanMaxInterval() {
|
||||||
|
Instant retryAfter = NOW.plus(MAX_INTERVAL.plusSeconds(10));
|
||||||
|
Instant result = calculator.onTooManyRequests(retryAfter, 1);
|
||||||
|
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,22 @@ class FeedCleanerTest {
|
|||||||
Assertions.assertEquals("<source>T´l´phone ′</source>", feedCleaner.replaceHtmlEntitiesWithNumericEntities(source));
|
Assertions.assertEquals("<source>T´l´phone ′</source>", feedCleaner.replaceHtmlEntitiesWithNumericEntities(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemoveDoctype() {
|
||||||
|
String source = "<!DOCTYPE html><html><head></head><body></body></html>";
|
||||||
|
Assertions.assertEquals("<html><head></head><body></body></html>", feedCleaner.removeDoctypeDeclarations(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemoveMultilineDoctype() {
|
||||||
|
String source = """
|
||||||
|
<!DOCTYPE
|
||||||
|
html
|
||||||
|
>
|
||||||
|
<html><head></head><body></body></html>""";
|
||||||
|
Assertions.assertEquals("""
|
||||||
|
|
||||||
|
<html><head></head><body></body></html>""", feedCleaner.removeDoctypeDeclarations(source));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.commafeed.backend.urlprovider;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class YoutubeFeedURLProviderTest {
|
||||||
|
|
||||||
|
private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesYoutubeChannelURL() {
|
||||||
|
Assertions.assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=abc",
|
||||||
|
provider.get("https://www.youtube.com/channel/abc", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotmatchYoutubeChannelURL() {
|
||||||
|
Assertions.assertNull(provider.get("https://www.anothersite.com/channel/abc", null));
|
||||||
|
Assertions.assertNull(provider.get("https://www.youtube.com/user/abc", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
|
import org.awaitility.Awaitility;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -14,6 +16,7 @@ import org.mockserver.integration.ClientAndServer;
|
|||||||
import org.mockserver.model.HttpRequest;
|
import org.mockserver.model.HttpRequest;
|
||||||
import org.mockserver.model.HttpResponse;
|
import org.mockserver.model.HttpResponse;
|
||||||
|
|
||||||
|
import com.commafeed.frontend.model.Entries;
|
||||||
import com.microsoft.playwright.Browser;
|
import com.microsoft.playwright.Browser;
|
||||||
import com.microsoft.playwright.Locator;
|
import com.microsoft.playwright.Locator;
|
||||||
import com.microsoft.playwright.Page;
|
import com.microsoft.playwright.Page;
|
||||||
@@ -22,6 +25,7 @@ import com.microsoft.playwright.assertions.PlaywrightAssertions;
|
|||||||
import com.microsoft.playwright.options.AriaRole;
|
import com.microsoft.playwright.options.AriaRole;
|
||||||
|
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
class ReadingIT {
|
class ReadingIT {
|
||||||
@@ -40,11 +44,15 @@ class ReadingIT {
|
|||||||
.respond(HttpResponse.response()
|
.respond(HttpResponse.response()
|
||||||
.withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))
|
.withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))
|
||||||
.withDelay(TimeUnit.MILLISECONDS, 100));
|
.withDelay(TimeUnit.MILLISECONDS, 100));
|
||||||
|
|
||||||
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
playwright.close();
|
playwright.close();
|
||||||
|
|
||||||
|
RestAssured.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -70,18 +78,28 @@ class ReadingIT {
|
|||||||
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
|
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
|
||||||
|
|
||||||
// we have two unread entries
|
// we have two unread entries
|
||||||
PlaywrightAssertions.assertThat(main.locator(".mantine-Paper-root")).hasCount(2);
|
PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(2);
|
||||||
|
|
||||||
// click on first entry
|
// click on first entry
|
||||||
main.getByText("Item 1").click();
|
main.getByText("Item 1").click();
|
||||||
PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1);
|
PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1);
|
||||||
PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(0);
|
PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(0);
|
||||||
|
|
||||||
|
// wait for the entry to be marked as read since the UI is updated immediately while the entry is marked as read in the background
|
||||||
|
Awaitility.await()
|
||||||
|
.atMost(15, TimeUnit.SECONDS)
|
||||||
|
.until(() -> RestAssured.given()
|
||||||
|
.get("rest/category/entries?id=all&readType=unread")
|
||||||
|
.then()
|
||||||
|
.statusCode(HttpStatus.SC_OK)
|
||||||
|
.extract()
|
||||||
|
.as(Entries.class), e -> e.getEntries().size() == 1);
|
||||||
|
|
||||||
// click on subscription
|
// click on subscription
|
||||||
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
|
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d*")).click();
|
||||||
|
|
||||||
// only one unread entry now
|
// only one unread entry now
|
||||||
PlaywrightAssertions.assertThat(main.locator(".mantine-Paper-root")).hasCount(1);
|
PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(1);
|
||||||
|
|
||||||
// click on second entry
|
// click on second entry
|
||||||
main.getByText("Item 2").click();
|
main.getByText("Item 2").click();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -41,6 +42,11 @@ class WebSocketIT extends BaseIT {
|
|||||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
|
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
|
||||||
AtomicBoolean connected = new AtomicBoolean();
|
AtomicBoolean connected = new AtomicBoolean();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.commafeed.integration.rest;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
@@ -25,6 +26,11 @@ class AdminIT extends BaseIT {
|
|||||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class Users {
|
class Users {
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.util.Objects;
|
|||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
@@ -38,6 +39,11 @@ class FeedIT extends BaseIT {
|
|||||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class Fetch {
|
class Fetch {
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.commafeed.integration.rest;
|
package com.commafeed.integration.rest;
|
||||||
|
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -37,6 +38,11 @@ class FeverIT extends BaseIT {
|
|||||||
this.userId = user.getId();
|
this.userId = user.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void invalidApiKey() {
|
void invalidApiKey() {
|
||||||
FeverResponse response = fetch("feeds", "invalid-key");
|
FeverResponse response = fetch("feeds", "invalid-key");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.integration.rest;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -29,6 +30,11 @@ class UserIT extends BaseIT {
|
|||||||
mailbox.clear();
|
mailbox.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resetPassword() {
|
void resetPassword() {
|
||||||
PasswordResetRequest req = new PasswordResetRequest();
|
PasswordResetRequest req = new PasswordResetRequest();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.integration.servlet;
|
|||||||
|
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
import org.hamcrest.CoreMatchers;
|
import org.hamcrest.CoreMatchers;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ class CustomCodeIT extends BaseIT {
|
|||||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test() {
|
void test() {
|
||||||
// get settings
|
// get settings
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.commafeed.integration.servlet;
|
package com.commafeed.integration.servlet;
|
||||||
|
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ class NextUnreadIT extends BaseIT {
|
|||||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
RestAssured.reset();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test() {
|
void test() {
|
||||||
subscribeAndWaitForEntries(getFeedUrl());
|
subscribeAndWaitForEntries(getFeedUrl());
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.6.0</version>
|
||||||
<name>CommaFeed</name>
|
<name>CommaFeed</name>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
<!-- https://stackoverflow.com/a/33823355/ -->
|
<!-- https://stackoverflow.com/a/33823355/ -->
|
||||||
<showWarnings>true</showWarnings>
|
<showWarnings>true</showWarnings>
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
|
<!-- enable annotation processing -->
|
||||||
|
<arg>-proc:full</arg>
|
||||||
|
|
||||||
<!-- disable the "processing" linter because we have annotations that are processed at runtime -->
|
<!-- disable the "processing" linter because we have annotations that are processed at runtime -->
|
||||||
<!-- https://stackoverflow.com/a/76126981/ -->
|
<!-- https://stackoverflow.com/a/76126981/ -->
|
||||||
<!-- disable the "classfile" linter because it generates "file missing" warnings about annotations with the "provided" scope -->
|
<!-- disable the "classfile" linter because it generates "file missing" warnings about annotations with the "provided" scope -->
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"config:recommended",
|
"config:recommended",
|
||||||
"customManagers:mavenPropertyVersions",
|
"customManagers:mavenPropertyVersions",
|
||||||
"customManagers:biomeVersions",
|
"customManagers:biomeVersions",
|
||||||
|
"helpers:pinGitHubActionDigests",
|
||||||
":automergePatch",
|
":automergePatch",
|
||||||
":automergeBranch",
|
":automergeBranch",
|
||||||
":automergeRequireAllStatusChecks",
|
":automergeRequireAllStatusChecks",
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
"matchDatasources": "docker",
|
"matchDatasources": "docker",
|
||||||
"matchPackageNames": "ibm-semeru-runtimes",
|
"matchPackageNames": "ibm-semeru-runtimes",
|
||||||
"versioning": "regex:^open-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?([\\._+](?<build>(\\d\\.?)+))?(-(?<compatibility>.*))?$",
|
"versioning": "regex:^open-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?([\\._+](?<build>(\\d\\.?)+))?(-(?<compatibility>.*))?$",
|
||||||
"allowedVersions": "/^open-(?:8|11|17|21)(?:\\.|-|$)/"
|
"allowedVersions": "/^open-(?:8|11|17|21|25)(?:\\.|-|$)/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user