forked from Archives/Athou_commafeed
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
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on: [ push ]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 21
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }}
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-jvm
|
||||
path: commafeed-server/target/commafeed-*.zip
|
||||
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
## tags
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||
|
||||
## master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
build:
|
||||
if: github.event_name != 'pull_request' || github.actor != 'renovate[bot]' # renovate already triggers the build on pushes
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-latest", "ubuntu-22.04-arm", "windows-latest" ]
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Configure git to checkout as-is
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
uses: graalvm/setup-graalvm@b0cb26a8da53cb3e97cdc0c827d8e3071240e730 # v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: sudo apt-get install -y libgbm1
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
# Build & Test
|
||||
- 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
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||
with:
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-linux
|
||||
- build-windows
|
||||
- build
|
||||
- docker
|
||||
permissions:
|
||||
contents: write
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set the exec flag on the native executables
|
||||
run: chmod +x artifacts/*-runner
|
||||
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
artifacts: ./artifacts/*
|
||||
|
||||
- name: Update Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
||||
# 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]
|
||||
|
||||
- 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
|
||||
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)
|
||||
- a `.env` file in the working directory (keys in UPPER_CASE)
|
||||
|
||||
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
|
||||
|
||||
All [CommaFeed settings](commafeed-server/doc/commafeed.adoc) are optional and have sensible default values.
|
||||
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,
|
||||
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.
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
#### Hard limit
|
||||
#### Hard limit (`native` and `jvm` packages)
|
||||
|
||||
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`.
|
||||
|
||||
#### 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
|
||||
following parameters:
|
||||
@@ -137,7 +137,7 @@ and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-
|
||||
more
|
||||
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
|
||||
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": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>CommaFeed</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="custom_css.css" />
|
||||
<script type="text/javascript" src="custom_js.js"></script>
|
||||
|
||||
<title>CommaFeed</title>
|
||||
</head>
|
||||
<body>
|
||||
<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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lingui/core": "^4.11.4",
|
||||
"@lingui/macro": "^4.11.4",
|
||||
"@lingui/react": "^4.11.4",
|
||||
"@mantine/core": "^7.13.2",
|
||||
"@mantine/form": "^7.13.2",
|
||||
"@mantine/hooks": "^7.13.2",
|
||||
"@mantine/modals": "^7.13.2",
|
||||
"@mantine/notifications": "^7.13.2",
|
||||
"@mantine/spotlight": "^7.13.2",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"axios": "^1.7.7",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fontsource/open-sans": "^5.1.1",
|
||||
"@mantine/core": "^7.16.3",
|
||||
"@mantine/form": "^7.16.3",
|
||||
"@mantine/hooks": "^7.16.3",
|
||||
"@mantine/modals": "^7.16.3",
|
||||
"@mantine/notifications": "^7.16.3",
|
||||
"@mantine/spotlight": "^7.16.3",
|
||||
"@lingui/core": "^5.2.0",
|
||||
"@lingui/react": "^5.2.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"interweave": "^13.1.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"redoc": "^2.1.5",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"redoc": "^2.4.0",
|
||||
"style-to-object": "^1.0.8",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.13",
|
||||
"tss-react": "^4.9.15",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.3",
|
||||
"@lingui/cli": "^4.11.4",
|
||||
"@lingui/vite-plugin": "^4.11.4",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
|
||||
"@lingui/cli": "^5.2.0",
|
||||
"@lingui/vite-plugin": "^5.2.0",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.5",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8",
|
||||
"jsdom": "^26.0.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.1.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.5",
|
||||
"vitest-mock-extended": "^2.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"react-infinite-scroller": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<version>5.6.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<properties>
|
||||
<!-- 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 -->
|
||||
<npm.version>10.9.0</npm.version>
|
||||
<npm.version>11.1.0</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -32,7 +32,6 @@ import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||
import React, { useEffect } from "react"
|
||||
import { isSafari } from "react-device-detect"
|
||||
import ReactGA from "react-ga4"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||
import Tinycon from "tinycon"
|
||||
|
||||
@@ -143,7 +142,7 @@ function GoogleAnalyticsHandler() {
|
||||
}
|
||||
|
||||
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 }) {
|
||||
@@ -170,15 +169,6 @@ function BrowserExtensionBadgeUnreadCountHandler() {
|
||||
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() {
|
||||
useI18n()
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
@@ -202,7 +192,6 @@ export function App() {
|
||||
<GoogleAnalyticsHandler />
|
||||
<RedirectHandler />
|
||||
<AppRoutes />
|
||||
<CustomCode />
|
||||
{/* 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
|
||||
https://github.com/Athou/commafeed/issues/1168
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import type { IconType } from "react-icons"
|
||||
import { FaAt } from "react-icons/fa"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
|
||||
import type { Category, Entry, SharingSettings } from "./types"
|
||||
|
||||
const categories: Record<string, Category> = {
|
||||
const categories: Record<string, Omit<Category, "name">> = {
|
||||
all: {
|
||||
id: "all",
|
||||
name: t`All`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
@@ -15,7 +13,6 @@ const categories: Record<string, Category> = {
|
||||
},
|
||||
starred: {
|
||||
id: "starred",
|
||||
name: t`Starred`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
|
||||
@@ -11,6 +11,7 @@ import { flushSync } from "react-dom"
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||
|
||||
export const loadEntries = createAppAsyncThunk(
|
||||
"entries/load",
|
||||
async (
|
||||
@@ -28,6 +29,7 @@ export const loadEntries = createAppAsyncThunk(
|
||||
return result.data
|
||||
}
|
||||
)
|
||||
|
||||
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { source } = state.entries
|
||||
@@ -37,6 +39,7 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
|
||||
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
|
||||
return result.data
|
||||
})
|
||||
|
||||
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
||||
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||
order: state.user.settings?.readingOrder,
|
||||
@@ -46,15 +49,18 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
|
||||
export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
|
||||
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(setSearch(arg))
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
|
||||
export const markEntry = createAppAsyncThunk(
|
||||
"entries/entry/mark",
|
||||
(arg: { entry: Entry; read: boolean }) => {
|
||||
@@ -67,6 +73,7 @@ export const markEntry = createAppAsyncThunk(
|
||||
condition: arg => arg.entry.markable && arg.entry.read !== arg.read,
|
||||
}
|
||||
)
|
||||
|
||||
export const markMultipleEntries = createAppAsyncThunk(
|
||||
"entries/entry/markMultiple",
|
||||
async (
|
||||
@@ -84,6 +91,7 @@ export const markMultipleEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
@@ -98,6 +106,7 @@ export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export const markAllEntries = createAppAsyncThunk(
|
||||
"entries/entry/markAll",
|
||||
async (
|
||||
@@ -113,6 +122,7 @@ export const markAllEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
|
||||
export const starEntry = createAppAsyncThunk(
|
||||
"entries/entry/star",
|
||||
(arg: { entry: Entry; starred: boolean }) => {
|
||||
@@ -126,6 +136,7 @@ export const starEntry = createAppAsyncThunk(
|
||||
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
|
||||
}
|
||||
)
|
||||
|
||||
export const selectEntry = createAppAsyncThunk(
|
||||
"entries/entry/select",
|
||||
(
|
||||
@@ -191,6 +202,7 @@ export const selectEntry = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const offset = (header?.bottom ?? 0) + margin
|
||||
@@ -228,6 +240,7 @@ export const selectPreviousEntry = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectNextEntry = createAppAsyncThunk(
|
||||
"entries/entry/selectNext",
|
||||
async (
|
||||
@@ -261,6 +274,7 @@ export const selectNextEntry = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
|
||||
await client.entry.tag(arg)
|
||||
thunkApi.dispatch(reloadTags())
|
||||
|
||||
@@ -3,43 +3,55 @@ import { Constants } from "app/constants"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
|
||||
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||
|
||||
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 redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
|
||||
const { source } = thunkApi.getState().entries
|
||||
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
||||
})
|
||||
|
||||
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||
)
|
||||
|
||||
export const redirectToRootCategory = createAppAsyncThunk(
|
||||
"redirect/category/root",
|
||||
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||
)
|
||||
|
||||
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||
)
|
||||
|
||||
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
||||
)
|
||||
|
||||
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||
)
|
||||
|
||||
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) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
||||
)
|
||||
|
||||
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 redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
||||
)
|
||||
|
||||
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
|
||||
)
|
||||
|
||||
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
|
||||
|
||||
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { incrementUnreadCount } from "app/tree/slice"
|
||||
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 collapseTreeCategory = createAppAsyncThunk(
|
||||
"tree/category/collapse",
|
||||
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 { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
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"
|
||||
|
||||
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 reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
|
||||
|
||||
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingMode })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
})
|
||||
|
||||
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingOrder })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
})
|
||||
|
||||
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, language })
|
||||
})
|
||||
|
||||
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||
})
|
||||
|
||||
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, showRead })
|
||||
})
|
||||
|
||||
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
|
||||
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMode })
|
||||
})
|
||||
|
||||
export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
||||
"settings/entriesToKeepOnTopWhenScrolling",
|
||||
(entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
|
||||
@@ -51,6 +61,7 @@ export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
||||
client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||
"settings/starIconDisplayMode",
|
||||
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||
@@ -59,6 +70,7 @@ export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||
client.user.saveSettings({ ...settings, starIconDisplayMode })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
||||
"settings/externalLinkIconDisplayMode",
|
||||
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||
@@ -67,6 +79,7 @@ export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
||||
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
"settings/markAllAsReadConfirmation",
|
||||
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
||||
@@ -75,26 +88,31 @@ export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, customContextMenu })
|
||||
})
|
||||
|
||||
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, mobileFooter })
|
||||
})
|
||||
|
||||
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountTitle })
|
||||
})
|
||||
|
||||
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountFavicon })
|
||||
})
|
||||
|
||||
export const changeSharingSetting = createAppAsyncThunk(
|
||||
"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 { Fragment } from "react"
|
||||
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 { useAppDispatch, useAppSelector } from "app/store"
|
||||
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 = () => {
|
||||
return (
|
||||
<Helmet>
|
||||
<style type="text/css">
|
||||
{`
|
||||
html, body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</Helmet>
|
||||
)
|
||||
import("./DisablePullToRefresh.css")
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ImageWithPlaceholderWhileLoadingProps {
|
||||
title?: string
|
||||
width?: number
|
||||
height?: number | "auto"
|
||||
style?: React.CSSProperties
|
||||
placeholderWidth?: number
|
||||
placeholderHeight?: number
|
||||
placeholderBackgroundColor?: string
|
||||
@@ -42,6 +43,7 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
src,
|
||||
title,
|
||||
width,
|
||||
style,
|
||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||
const { classes } = useStyles({
|
||||
placeholderWidth,
|
||||
@@ -68,7 +70,11 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
width={width}
|
||||
height={height}
|
||||
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 { useOs } from "@mantine/hooks"
|
||||
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 { Constants } from "app/constants"
|
||||
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 { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { TypographyStylesProvider } from "@mantine/core"
|
||||
import type { ReactNode } from "react"
|
||||
import { tss } from "tss"
|
||||
|
||||
/**
|
||||
* This component is used to provide basic styles to html typography elements.
|
||||
*
|
||||
* 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 }) => {
|
||||
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 { type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
||||
import React from "react"
|
||||
import styleToObject from "style-to-object"
|
||||
import { tss } from "tss"
|
||||
|
||||
export interface ContentProps {
|
||||
@@ -42,6 +43,7 @@ const transform: TransformCallback = node => {
|
||||
const nodeHeight = node.getAttribute("height")
|
||||
const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
|
||||
const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
|
||||
const style = styleToObject(node.getAttribute("style") ?? "") ?? undefined
|
||||
const placeholderSize = calculatePlaceholderSize({
|
||||
width,
|
||||
height,
|
||||
@@ -55,6 +57,7 @@ const transform: TransformCallback = node => {
|
||||
title={title}
|
||||
width={width}
|
||||
height="auto"
|
||||
style={style}
|
||||
placeholderWidth={placeholderSize.width}
|
||||
placeholderHeight={placeholderSize.height}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function Enclosure(props: {
|
||||
)}
|
||||
{hasAudio && (
|
||||
// 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} />
|
||||
</audio>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box } from "@mantine/core"
|
||||
import { openModal } from "@mantine/modals"
|
||||
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 { Constants } from "app/constants"
|
||||
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 { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||
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 { Constants } from "app/constants"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
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 { Select, type SelectProps } from "@mantine/core"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
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 { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -39,8 +39,8 @@ export function Subscribe() {
|
||||
},
|
||||
})
|
||||
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
||||
onSuccess: sub => {
|
||||
dispatch(reloadTree())
|
||||
onSuccess: async sub => {
|
||||
await dispatch(reloadTree())
|
||||
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 { Constants } from "app/constants"
|
||||
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 { Constants } from "app/constants"
|
||||
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 { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
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 { markAllEntries } from "app/entries/thunks"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
Box,
|
||||
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 { useForm } from "@mantine/form"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Divider, Group, NumberInput, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { TextInput } from "@mantine/core"
|
||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||
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 { useAppSelector } from "app/store"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { setWebSocketConnected } from "app/server/slice"
|
||||
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 WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||
|
||||
@@ -9,7 +9,7 @@ const handleMessage = (dispatch: AppDispatch, message: string) => {
|
||||
const type = parts[0]
|
||||
if (type === "new-feed-entries") {
|
||||
dispatch(
|
||||
incrementUnreadCount({
|
||||
newFeedEntriesDiscovered({
|
||||
feedId: +parts[1],
|
||||
amount: +parts[2],
|
||||
})
|
||||
|
||||
@@ -142,7 +142,7 @@ msgstr "浏览器扩展"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
msgstr "浏览器标签页"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -323,7 +323,7 @@ msgstr "输入您当前的密码以更改配置文件设置"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
msgstr "滚动时固定在顶部的条目"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
@@ -378,7 +378,7 @@ msgstr "过滤表达式"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
msgstr "强制获取订阅源功能不可用。"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
@@ -844,11 +844,11 @@ msgstr "显示星标图标"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
msgstr "在标签页图标上显示未读数量"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
msgstr "在标签页标题中显示未读数量"
|
||||
|
||||
#: 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 { TbRefresh } from "react-icons/tb"
|
||||
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 { client } from "app/client"
|
||||
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 { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
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 { AddCategory } from "components/content/add/AddCategory"
|
||||
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 { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
@@ -19,6 +21,7 @@ import { useParams } from "react-router-dom"
|
||||
|
||||
export function CategoryDetailsPage() {
|
||||
const { id = Constants.categories.all.id } = useParams()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -26,7 +29,7 @@ export function CategoryDetailsPage() {
|
||||
const query = useAsync(async () => await client.category.getRoot(), [])
|
||||
const category =
|
||||
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)
|
||||
|
||||
const form = useForm<CategoryModificationRequest>()
|
||||
@@ -63,14 +66,14 @@ export function CategoryDetailsPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!category) return
|
||||
if (!category?.id) return
|
||||
setValues({
|
||||
id: +category.id,
|
||||
name: category.name,
|
||||
parentId: category.parentId,
|
||||
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
|
||||
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 { Constants } from "app/constants"
|
||||
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 { useForm } from "@mantine/form"
|
||||
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 { useViewportSize } from "@mantine/hooks"
|
||||
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 { Constants } from "app/constants"
|
||||
import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
|
||||
@@ -18,7 +18,7 @@ import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useWebSocket } from "hooks/useWebSocket"
|
||||
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 { TbMenu2, TbPlus, TbX } from "react-icons/tb"
|
||||
import { Outlet } from "react-router-dom"
|
||||
@@ -185,7 +185,7 @@ export default function Layout(props: LayoutProps) {
|
||||
</AppShell.Navbar>
|
||||
<OnDesktop>
|
||||
<Draggable
|
||||
nodeRef={draggableSeparator}
|
||||
nodeRef={draggableSeparator as RefObject<HTMLElement>}
|
||||
axis="x"
|
||||
defaultPosition={{
|
||||
x: sidebarWidth,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Container, Tabs } from "@mantine/core"
|
||||
import { CustomCodeSettings } from "components/settings/CustomCodeSettings"
|
||||
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 { 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 { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
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 { useForm } from "@mantine/form"
|
||||
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 { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
|
||||
@@ -10,8 +10,7 @@ export default defineConfig(() => ({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
// babel-macro is needed for lingui
|
||||
plugins: ["macros"],
|
||||
plugins: ["@lingui/babel-plugin-lingui-macro"],
|
||||
},
|
||||
}),
|
||||
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>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<version>5.6.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<quarkus.version>3.15.1</quarkus.version>
|
||||
<querydsl.version>6.8</querydsl.version>
|
||||
<quarkus.version>3.18.3</quarkus.version>
|
||||
<querydsl.version>6.10.1</querydsl.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>
|
||||
</properties>
|
||||
@@ -43,7 +43,7 @@
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-help-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>initialize</phase>
|
||||
@@ -104,11 +104,14 @@
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-generate-asciidoc</id>
|
||||
<id>default-generate-config-doc</id>
|
||||
<phase>process-test-resources</phase>
|
||||
<goals>
|
||||
<goal>generate-asciidoc</goal>
|
||||
<goal>generate-config-doc</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<format>markdown</format>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
@@ -135,7 +138,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.5.2</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
@@ -146,7 +149,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.5.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -233,12 +236,12 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>10.18.2</version>
|
||||
<version>10.21.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
@@ -267,7 +270,7 @@
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.43.0</version>
|
||||
<version>2.44.2</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -294,14 +297,14 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<version>5.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- compile-time processors -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
<version>1.18.36</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -358,7 +361,7 @@
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
<version>4.2.28</version>
|
||||
<version>4.2.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
@@ -405,7 +408,7 @@
|
||||
<dependency>
|
||||
<groupId>org.passay</groupId>
|
||||
<artifactId>passay</artifactId>
|
||||
<version>1.6.5</version>
|
||||
<version>1.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.rometools</groupId>
|
||||
@@ -430,12 +433,12 @@
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.18.1</version>
|
||||
<version>1.18.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>75.1</version>
|
||||
<version>76.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.cssparser</groupId>
|
||||
@@ -450,7 +453,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.4</version>
|
||||
<version>5.4.2</version>
|
||||
</dependency>
|
||||
<!-- add brotli support for httpclient5 -->
|
||||
<dependency>
|
||||
@@ -461,7 +464,7 @@
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||
<version>8.3.7</version>
|
||||
<version>9.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- test dependencies -->
|
||||
@@ -489,7 +492,7 @@
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.47.0</version>
|
||||
<version>1.50.0</version>
|
||||
<scope>test</scope>
|
||||
</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
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/quarkus-app/ /commafeed
|
||||
COPY artifacts/extracted-jvm-package/quarkus-app/ /commafeed
|
||||
WORKDIR /commafeed
|
||||
|
||||
CMD ["java", \
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
FROM debian:12.7
|
||||
FROM debian:12.9
|
||||
ARG TARGETARCH
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/commafeed-*-runner /commafeed/application
|
||||
COPY artifacts/commafeed-*-${TARGETARCH}-runner /commafeed/application
|
||||
WORKDIR /commafeed
|
||||
|
||||
CMD ["./application"]
|
||||
|
||||
@@ -68,7 +68,7 @@ CommaFeed also supports:
|
||||
|
||||
## 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.
|
||||
|
||||
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)
|
||||
- `master` (always points to the latest git commit)
|
||||
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports
|
||||
the arm64 platform which is not yet supported by the native image.
|
||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively.
|
||||
|
||||
@@ -138,6 +138,16 @@ public interface CommaFeedConfiguration {
|
||||
@WithDefault("5M")
|
||||
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
|
||||
*/
|
||||
@@ -168,20 +178,39 @@ public interface CommaFeedConfiguration {
|
||||
|
||||
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")
|
||||
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
|
||||
* last entry was published. The interval will be somewhere between the default refresh interval and 24h.
|
||||
* 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>
|
||||
*/
|
||||
@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.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
@WithDefault("true")
|
||||
boolean intervalEmpirical();
|
||||
|
||||
/**
|
||||
* Feed refresh engine error handling settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
FeedRefreshErrorHandling errors();
|
||||
|
||||
/**
|
||||
* Amount of http threads used to fetch feeds.
|
||||
*/
|
||||
@@ -217,6 +246,21 @@ public interface CommaFeedConfiguration {
|
||||
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 {
|
||||
/**
|
||||
* Timeout applied to all database queries.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.time.InstantSource;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
@@ -8,9 +10,16 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
public class CommaFeedProducers {
|
||||
|
||||
@Produces
|
||||
@Singleton
|
||||
public InstantSource instantSource() {
|
||||
return InstantSource.system();
|
||||
}
|
||||
|
||||
@Produces
|
||||
@Singleton
|
||||
public MetricRegistry metricRegistry() {
|
||||
return new MetricRegistry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,14 +2,21 @@ package com.commafeed.backend;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.InstantSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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.RequestConfig;
|
||||
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.protocol.HttpClientContext;
|
||||
import org.apache.hc.client5.http.protocol.RedirectLocations;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
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.util.TimeValue;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
@@ -41,9 +50,11 @@ import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.ws.rs.core.CacheControl;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.altindag.ssl.SSLFactory;
|
||||
@@ -57,11 +68,14 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
||||
public class HttpGetter {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
private final InstantSource instantSource;
|
||||
private final CloseableHttpClient client;
|
||||
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.instantSource = instantSource;
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
if (cache == null) {
|
||||
response = invoke(request);
|
||||
@@ -102,9 +125,15 @@ public class HttpGetter {
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -118,8 +147,35 @@ public class HttpGetter {
|
||||
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,
|
||||
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 {
|
||||
@@ -147,6 +203,18 @@ public class HttpGetter {
|
||||
.map(StringUtils::trimToNull)
|
||||
.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 urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
|
||||
.map(RedirectLocations::getAll)
|
||||
@@ -154,10 +222,31 @@ public class HttpGetter {
|
||||
.map(URI::toString)
|
||||
.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 {
|
||||
if (entity.getContentLength() > maxBytes) {
|
||||
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();
|
||||
|
||||
int poolSize = config.feedRefresh().httpThreads();
|
||||
@@ -191,6 +280,7 @@ public class HttpGetter {
|
||||
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
|
||||
.setMaxConnPerRoute(poolSize)
|
||||
.setMaxConnTotal(poolSize)
|
||||
.setDnsResolver(dnsResolver)
|
||||
.build();
|
||||
|
||||
}
|
||||
@@ -227,6 +317,22 @@ public class HttpGetter {
|
||||
.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
|
||||
public static class NotModifiedException extends Exception {
|
||||
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
|
||||
public static class HttpResponseException extends IOException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -293,6 +407,8 @@ public class HttpGetter {
|
||||
int code;
|
||||
String lastModifiedHeader;
|
||||
String eTagHeader;
|
||||
CacheControl cacheControl;
|
||||
Instant retryAfter;
|
||||
byte[] content;
|
||||
String contentType;
|
||||
String urlAfterRedirect;
|
||||
@@ -305,6 +421,7 @@ public class HttpGetter {
|
||||
String lastModifiedSince;
|
||||
String eTag;
|
||||
String urlAfterRedirect;
|
||||
Duration validFor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ import org.apache.hc.core5.net.URIBuilder;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
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.fasterxml.jackson.core.JsonPointer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -91,7 +94,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
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")
|
||||
.queryParam("part", "snippet")
|
||||
.queryParam("key", googleAuthKey)
|
||||
@@ -100,7 +104,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
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")
|
||||
.queryParam("part", "snippet")
|
||||
.queryParam("key", googleAuthKey)
|
||||
@@ -109,7 +114,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
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")
|
||||
.queryParam("part", "snippet")
|
||||
.queryParam("key", googleAuthKey)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.feed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@@ -9,9 +10,12 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.commafeed.backend.Digests;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
|
||||
import com.commafeed.backend.HttpGetter.HttpRequest;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
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.FeedParserResult;
|
||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||
@@ -39,7 +43,8 @@ public class FeedFetcher {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
|
||||
@@ -87,7 +92,8 @@ public class FeedFetcher {
|
||||
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) {
|
||||
@@ -102,7 +108,7 @@ public class FeedFetcher {
|
||||
}
|
||||
|
||||
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.Instant;
|
||||
import java.time.InstantSource;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class FeedRefreshIntervalCalculator {
|
||||
|
||||
private final Duration refreshInterval;
|
||||
private final boolean empiricalInterval;
|
||||
private final Duration interval;
|
||||
private final Duration maxInterval;
|
||||
private final boolean empirical;
|
||||
private final FeedRefreshErrorHandling errorHandling;
|
||||
private final InstantSource instantSource;
|
||||
|
||||
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {
|
||||
this.refreshInterval = config.feedRefresh().interval();
|
||||
this.empiricalInterval = config.feedRefresh().intervalEmpirical();
|
||||
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config, InstantSource instantSource) {
|
||||
this.interval = config.feedRefresh().interval();
|
||||
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) {
|
||||
Instant defaultRefreshInterval = getDefaultRefreshInterval();
|
||||
return empiricalInterval ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval, defaultRefreshInterval)
|
||||
: defaultRefreshInterval;
|
||||
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval, Duration validFor) {
|
||||
Instant instant = empirical ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
|
||||
: instantSource.instant().plus(interval);
|
||||
return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor)));
|
||||
}
|
||||
|
||||
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) {
|
||||
int retriesBeforeDisable = 3;
|
||||
if (errorCount < retriesBeforeDisable || !empiricalInterval) {
|
||||
return getDefaultRefreshInterval();
|
||||
if (errorCount < errorHandling.retriesBeforeBackoff()) {
|
||||
return constrainToBounds(instantSource.instant().plus(interval));
|
||||
}
|
||||
|
||||
int disabledHours = Math.min(24 * 7, errorCount - retriesBeforeDisable + 1);
|
||||
return Instant.now().plus(Duration.ofHours(disabledHours));
|
||||
Duration retryInterval = errorHandling.backoffInterval().multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
|
||||
return constrainToBounds(instantSource.instant().plus(retryInterval));
|
||||
}
|
||||
|
||||
private Instant getDefaultRefreshInterval() {
|
||||
return Instant.now().plus(refreshInterval);
|
||||
}
|
||||
|
||||
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
|
||||
Instant now = Instant.now();
|
||||
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval) {
|
||||
Instant now = instantSource.instant();
|
||||
|
||||
if (publishedDate == null) {
|
||||
// feed with no entries, recheck in 24 hours
|
||||
return now.plus(Duration.ofHours(24));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 30) {
|
||||
// older than a month, recheck in 24 hours
|
||||
return now.plus(Duration.ofHours(24));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 14) {
|
||||
// older than two weeks, recheck in 12 hours
|
||||
return now.plus(Duration.ofHours(12));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 7) {
|
||||
// older than a week, recheck in 6 hours
|
||||
return now.plus(Duration.ofHours(6));
|
||||
return now.plus(maxInterval);
|
||||
}
|
||||
|
||||
long daysSinceLastPublication = ChronoUnit.DAYS.between(publishedDate, now);
|
||||
if (daysSinceLastPublication >= 30) {
|
||||
return now.plus(maxInterval);
|
||||
} else if (daysSinceLastPublication >= 14) {
|
||||
return now.plus(maxInterval.dividedBy(2));
|
||||
} else if (daysSinceLastPublication >= 7) {
|
||||
return now.plus(maxInterval.dividedBy(4));
|
||||
} else if (averageEntryInterval != null) {
|
||||
// use average time between entries to decide when to refresh next, divided by factor
|
||||
int factor = 2;
|
||||
|
||||
// not more than 6 hours
|
||||
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);
|
||||
long millis = Longs.constrainToRange(averageEntryInterval / factor, interval.toMillis(), maxInterval.dividedBy(4).toMillis());
|
||||
return now.plusMillis(millis);
|
||||
} else {
|
||||
// unknown case, recheck in 24 hours
|
||||
return now.plus(Duration.ofHours(24));
|
||||
// unknown case
|
||||
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.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
@@ -76,8 +77,8 @@ public class FeedRefreshWorker {
|
||||
|
||||
feed.setErrorCount(0);
|
||||
feed.setMessage(null);
|
||||
feed.setDisabledUntil(
|
||||
refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(), result.feed().averageEntryInterval()));
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(),
|
||||
result.feed().averageEntryInterval(), result.validFor()));
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, entries);
|
||||
} catch (NotModifiedException e) {
|
||||
@@ -95,6 +96,14 @@ public class FeedRefreshWorker {
|
||||
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());
|
||||
} catch (Exception e) {
|
||||
log.debug("unable to refresh feed {}", feed.getUrl(), e);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.backend.feed.parser;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.ahocorasick.trie.Emit;
|
||||
import org.ahocorasick.trie.Trie;
|
||||
@@ -11,6 +12,8 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
class FeedCleaner {
|
||||
|
||||
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public String trimInvalidXmlCharacters(String xml) {
|
||||
if (StringUtils.isBlank(xml)) {
|
||||
return null;
|
||||
@@ -60,4 +63,8 @@ class FeedCleaner {
|
||||
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);
|
||||
}
|
||||
xmlString = feedCleaner.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
||||
xmlString = feedCleaner.removeDoctypeDeclarations(xmlString);
|
||||
|
||||
InputSource source = new InputSource(new StringReader(xmlString));
|
||||
SyndFeed feed = new SyndFeedInput().build(source);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.commafeed.backend.urlprovider;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -14,12 +13,16 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
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
|
||||
public String get(String url, String urlContent) {
|
||||
Matcher matcher = REGEXP.matcher(url);
|
||||
return matcher.find() ? matcher.group(1) + "/feeds/videos.xml?channel_id=" + matcher.group(2) : null;
|
||||
if (!StringUtils.startsWithIgnoreCase(url, PREFIX)) {
|
||||
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();
|
||||
}
|
||||
|
||||
String passwd = RandomStringUtils.randomAlphanumeric(10);
|
||||
String passwd = RandomStringUtils.secure().nextAlphanumeric(10);
|
||||
byte[] encryptedPassword = encryptionService.getEncryptedPassword(passwd, user.getSalt());
|
||||
user.setPassword(encryptedPassword);
|
||||
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.http-only-cookie=true
|
||||
quarkus.http.auth.form.timeout=P30d
|
||||
quarkus.http.auth.form.cookie-max-age=P30d
|
||||
quarkus.http.auth.form.landing-page=
|
||||
quarkus.http.auth.form.login-page=
|
||||
quarkus.http.auth.form.error-page=
|
||||
@@ -49,6 +50,8 @@ quarkus.native.add-all-charsets=true
|
||||
%test.commafeed.users.allow-registrations=true
|
||||
%test.commafeed.password-recovery-enabled=true
|
||||
%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
|
||||
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import org.junit.jupiter.api.Test;
|
||||
class CommaFeedConfigurationTest {
|
||||
|
||||
@Test
|
||||
void verifyAsciiDocIsUpToDate() throws IOException {
|
||||
String versionedDocumentationFile = FileUtils.readFileToString(new File("doc/commafeed.adoc"), StandardCharsets.UTF_8);
|
||||
String generatedDocumentationFile = FileUtils
|
||||
.readFileToString(new File("target/quarkus-generated-doc/config/commafeed-server.adoc"), StandardCharsets.UTF_8);
|
||||
void verifyMarkdownDocIsUpToDate() throws IOException {
|
||||
String versionedDocumentationFile = FileUtils.readFileToString(new File("doc/commafeed.md"), StandardCharsets.UTF_8);
|
||||
String generatedDocumentationFile = FileUtils.readFileToString(new File("target/quarkus-generated-doc/config/commafeed-server.md"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
Assertions.assertLinesMatch(versionedDocumentationFile.lines(), generatedDocumentationFile.lines());
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -39,6 +40,7 @@ import com.commafeed.CommaFeedVersion;
|
||||
import com.commafeed.backend.HttpGetter.HttpResponseException;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import io.quarkus.runtime.configuration.MemorySize;
|
||||
@@ -46,9 +48,12 @@ import io.quarkus.runtime.configuration.MemorySize;
|
||||
@ExtendWith(MockServerExtension.class)
|
||||
class HttpGetterTest {
|
||||
|
||||
private static final Instant NOW = Instant.now();
|
||||
|
||||
private MockServerClient mockServerClient;
|
||||
private String feedUrl;
|
||||
private byte[] feedContent;
|
||||
|
||||
private CommaFeedConfiguration config;
|
||||
|
||||
private HttpGetter getter;
|
||||
@@ -73,7 +78,7 @@ class HttpGetterTest {
|
||||
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
|
||||
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
|
||||
@@ -93,16 +98,52 @@ class HttpGetterTest {
|
||||
.withBody(feedContent)
|
||||
.withContentType(MediaType.APPLICATION_ATOM_XML)
|
||||
.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);
|
||||
Assertions.assertArrayEquals(feedContent, result.getContent());
|
||||
Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.getContentType());
|
||||
Assertions.assertEquals("123456", result.getLastModifiedSince());
|
||||
Assertions.assertEquals("78910", result.getETag());
|
||||
Assertions.assertEquals(Duration.ofSeconds(60), result.getValidFor());
|
||||
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
|
||||
@ValueSource(
|
||||
ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
|
||||
@@ -131,7 +172,7 @@ class HttpGetterTest {
|
||||
@Test
|
||||
void dataTimeout() {
|
||||
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"))
|
||||
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
|
||||
@@ -142,7 +183,7 @@ class HttpGetterTest {
|
||||
@Test
|
||||
void connectTimeout() {
|
||||
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
|
||||
// https://stackoverflow.com/a/904609
|
||||
Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.get("http://10.255.255.1"));
|
||||
@@ -195,7 +236,7 @@ class HttpGetterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheSubsequentCalls() throws IOException, NotModifiedException {
|
||||
void cacheSubsequentCalls() throws Exception {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
|
||||
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
|
||||
@@ -261,17 +302,16 @@ class HttpGetterTest {
|
||||
class Compression {
|
||||
|
||||
@Test
|
||||
void deflate() throws IOException, NotModifiedException {
|
||||
void deflate() throws Exception {
|
||||
supportsCompression("deflate", DeflaterOutputStream::new);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gzip() throws IOException, NotModifiedException {
|
||||
void gzip() throws Exception {
|
||||
supportsCompression("gzip", GZIPOutputStream::new);
|
||||
}
|
||||
|
||||
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction)
|
||||
throws IOException, NotModifiedException {
|
||||
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction) throws Exception {
|
||||
String body = "my body";
|
||||
|
||||
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;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@@ -46,7 +47,7 @@ class FeedFetcherTest {
|
||||
String lastContentHash = Digests.sha1Hex(content);
|
||||
|
||||
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,
|
||||
() -> 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));
|
||||
}
|
||||
|
||||
@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 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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -14,6 +16,7 @@ import org.mockserver.integration.ClientAndServer;
|
||||
import org.mockserver.model.HttpRequest;
|
||||
import org.mockserver.model.HttpResponse;
|
||||
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.microsoft.playwright.Browser;
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
@@ -22,6 +25,7 @@ import com.microsoft.playwright.assertions.PlaywrightAssertions;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
|
||||
@QuarkusTest
|
||||
class ReadingIT {
|
||||
@@ -40,11 +44,15 @@ class ReadingIT {
|
||||
.respond(HttpResponse.response()
|
||||
.withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))
|
||||
.withDelay(TimeUnit.MILLISECONDS, 100));
|
||||
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
playwright.close();
|
||||
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,18 +78,28 @@ class ReadingIT {
|
||||
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
|
||||
|
||||
// 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
|
||||
main.getByText("Item 1").click();
|
||||
PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1);
|
||||
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
|
||||
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
|
||||
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d*")).click();
|
||||
|
||||
// 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
|
||||
main.getByText("Item 2").click();
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -41,6 +42,11 @@ class WebSocketIT extends BaseIT {
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.integration.rest;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -25,6 +26,11 @@ class AdminIT extends BaseIT {
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Users {
|
||||
@Test
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.Objects;
|
||||
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.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -38,6 +39,11 @@ class FeedIT extends BaseIT {
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Fetch {
|
||||
@Test
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.integration.rest;
|
||||
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -37,6 +38,11 @@ class FeverIT extends BaseIT {
|
||||
this.userId = user.getId();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidApiKey() {
|
||||
FeverResponse response = fetch("feeds", "invalid-key");
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.integration.rest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -29,6 +30,11 @@ class UserIT extends BaseIT {
|
||||
mailbox.clear();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPassword() {
|
||||
PasswordResetRequest req = new PasswordResetRequest();
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.integration.servlet;
|
||||
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -20,6 +21,11 @@ class CustomCodeIT extends BaseIT {
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
// get settings
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.integration.servlet;
|
||||
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -18,6 +19,11 @@ class NextUnreadIT extends BaseIT {
|
||||
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
5
pom.xml
5
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<version>5.6.0</version>
|
||||
<name>CommaFeed</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<!-- https://stackoverflow.com/a/33823355/ -->
|
||||
<showWarnings>true</showWarnings>
|
||||
<compilerArgs>
|
||||
<!-- enable annotation processing -->
|
||||
<arg>-proc:full</arg>
|
||||
|
||||
<!-- disable the "processing" linter because we have annotations that are processed at runtime -->
|
||||
<!-- https://stackoverflow.com/a/76126981/ -->
|
||||
<!-- disable the "classfile" linter because it generates "file missing" warnings about annotations with the "provided" scope -->
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"config:recommended",
|
||||
"customManagers:mavenPropertyVersions",
|
||||
"customManagers:biomeVersions",
|
||||
"helpers:pinGitHubActionDigests",
|
||||
":automergePatch",
|
||||
":automergeBranch",
|
||||
":automergeRequireAllStatusChecks",
|
||||
@@ -31,7 +32,7 @@
|
||||
"matchDatasources": "docker",
|
||||
"matchPackageNames": "ibm-semeru-runtimes",
|
||||
"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