Compare commits
361 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0336774e8c | |||
| 756dd449cc | |||
| a0b4665080 | |||
| 6592c1ad6b | |||
| f109e3207c | |||
| b54fe66809 | |||
| 93c9d43497 | |||
| e3b3c40c28 | |||
| 068790fc19 | |||
| 41d99c517f | |||
| 3db3f9884f | |||
| ad1d349a0c | |||
| 7ee4a731af | |||
| 3c822a45ac | |||
| c2311e316c | |||
| ed20771194 | |||
| beb8c80787 | |||
| fc4bdf3ab0 | |||
| 6bc819e198 | |||
| 08640b27d5 | |||
| 4be993e055 | |||
| 9df12177eb | |||
| a78d1c79da | |||
| 1ea3953271 | |||
| fe8b7a907e | |||
| a15cdb1655 | |||
| fa9697b972 | |||
| 8e28e41b02 | |||
| e2ee74428a | |||
| 863a5b3648 | |||
| 91f5522ce0 | |||
| 8212f16aa2 | |||
| b1e73fafdf | |||
| f6404290ba | |||
| c29ca0e313 | |||
| 02a074ed26 | |||
| 00dabc1d3c | |||
| b68ada396a | |||
| e299e821a6 | |||
| 0f17dfc7d6 | |||
| 7c80d6b86d | |||
| f6f63b5282 | |||
| 309fdbb9fc | |||
| e2d862f2f6 | |||
| 4834e934f2 | |||
| dd4fb660c1 | |||
| 2ba893bc79 | |||
| 7a307f8e74 | |||
| 7310e13499 | |||
| bf6d286735 | |||
| ca919c2ff8 | |||
| 5948786b15 | |||
| f4746a7306 | |||
| 648b9a8f6f | |||
| 66b8483791 | |||
| e0ee28c013 | |||
| d862d86f90 | |||
| d25c032171 | |||
| 8429c6b0ab | |||
| 6bc4b35e38 | |||
| ee3486af66 | |||
| 45d486b919 | |||
| 688b73b7ae | |||
| 6d97f8b458 | |||
| f8e50947f2 | |||
| 9a637ce95e | |||
| d3a85b049b | |||
| 5bcb37901c | |||
| 9c8a7dfffe | |||
| 74e4032ffc | |||
| fd1fee852c | |||
| c51a3270da | |||
| 45fa641d26 | |||
| fd8f25916b | |||
| 826e4d654f | |||
| d9d17f0d69 | |||
| eaaeb68474 | |||
| 382885f144 | |||
| 0f7b047b0a | |||
| a074773e6c | |||
| 3d0126be0b | |||
| eda2e2f3f5 | |||
| 111e3f2106 | |||
| c1ec77a42c | |||
| 3339d9d3d7 | |||
| 8d80e9103f | |||
| d55b410800 | |||
| 9fe99ce7fa | |||
| b8df6c31a0 | |||
| abdd5876a1 | |||
| 1b5edfc00a | |||
| 347740dce1 | |||
| ab85d4d678 | |||
| 31ac62f410 | |||
| 97765b93a9 | |||
| f858ad5f26 | |||
| e6524f925f | |||
| c493f8921e | |||
| b2ce98da87 | |||
| 4db138d4b8 | |||
| f12d5131b0 | |||
| 1f5c8ce353 | |||
| 645a817685 | |||
| f4f8342245 | |||
| 543a690bfd | |||
| c4e5dad549 | |||
| fa12c23d79 | |||
| 4fe902a5d2 | |||
| 61af08a721 | |||
| b04550e2f2 | |||
| 5e5cb056c5 | |||
| 48fa64f8ec | |||
| f274394f0e | |||
| 9a4a942cc4 | |||
| 6b3b8e8c9b | |||
| 5a7d6f8997 | |||
| b4ed17fbac | |||
| 57476f4d59 | |||
| 7660910232 | |||
| b054506e3a | |||
| c961c6db7d | |||
| 0f126d4d11 | |||
| b94756bbf0 | |||
| db6ae707ef | |||
| 97feec8ebf | |||
| bce21a9f91 | |||
| 06e256e5ef | |||
| 040938ff6d | |||
| 21da7f77f5 | |||
| c2d2f31438 | |||
| 5b2558bf92 | |||
| ecd59009fb | |||
| 4a943b722d | |||
| 9d1b1e19d4 | |||
| 7a8061fc72 | |||
| bca84bac8b | |||
| 66e0eb1bd6 | |||
| 26d189917e | |||
| ccd3955bf4 | |||
| 8a2cc3a344 | |||
| 647fa025f8 | |||
| 1955350318 | |||
| 04916a57d2 | |||
| 0adac5c6f7 | |||
| 54b5be5e7d | |||
| eae4cb1417 | |||
| 420a3d4d95 | |||
| b48ad6dbfb | |||
| 2be5051b19 | |||
| c544dadd55 | |||
| 1da65d97d8 | |||
| c595c80356 | |||
| 20e5fbcd7a | |||
| ac77154907 | |||
| 97ace53bc9 | |||
| feb962f98a | |||
| 8602089a1e | |||
| 4b0648f3d7 | |||
| 856b96cbf8 | |||
| facf38955c | |||
| cfdb890eae | |||
| 2f8d3a7958 | |||
| 59311deb57 | |||
| d2541a173a | |||
| b618c11b80 | |||
| 8b4675807a | |||
| c0bca973d6 | |||
| 5c97771e61 | |||
| c9cbe8afd5 | |||
| 1d90ce9dd2 | |||
| ccb9eed573 | |||
| 2221fd408d | |||
| ea58bac548 | |||
| 0f85c0511a | |||
| 27749a2877 | |||
| 0991c27f9d | |||
| 00eab03655 | |||
| e55377b204 | |||
| 4ddc4ec002 | |||
| facf17db3f | |||
| 8663c7d031 | |||
| 6eac968083 | |||
| bd573957e0 | |||
| 5ce5c47499 | |||
| 9336891e67 | |||
| aa30c35e7e | |||
| 39368ece9a | |||
| 4f57309380 | |||
| 57e7bd5bc9 | |||
| bf54222be7 | |||
| 552fb3e4cc | |||
| 7d9f174b3f | |||
| bf4d31eebe | |||
| f203326a29 | |||
| 8367413e84 | |||
| 9b6dbd422c | |||
| 531e80f580 | |||
| 890a34e1bd | |||
| 7413e383a8 | |||
| 7496479380 | |||
| 6c78a1d635 | |||
| 6413c9f9f7 | |||
| 352aeb0490 | |||
| 61f52d971a | |||
| fa7508e28d | |||
| c217a31444 | |||
| 84576f2c29 | |||
| da11416b39 | |||
| 6a9a590c7f | |||
| f23e6a3352 | |||
| b568b1d41d | |||
| 9980634e5d | |||
| 4a50ca9122 | |||
| 3be0d14d44 | |||
| ec9fd996b1 | |||
| 1e704468a5 | |||
| e8147f26b9 | |||
| 6648e0af38 | |||
| fde84d55ba | |||
| 1159dd6982 | |||
| 50341759b6 | |||
| d68f2306c6 | |||
| 578743de1f | |||
| 8553188ae4 | |||
| a3e2570df2 | |||
| 87c9ef6b48 | |||
| ce32d181d5 | |||
| b8c6c64e9c | |||
| c51f092bda | |||
| e2d33f680e | |||
| 980c5c63df | |||
| 1441dc7600 | |||
| 6fc4e2f45e | |||
| 8c00dbcf38 | |||
| 803e160c70 | |||
| 4590da2fc3 | |||
| a1879ea37c | |||
| 8fe289ca72 | |||
| eb9ac861ea | |||
| 074393d3bf | |||
| 538e5305d3 | |||
| 917852bbb0 | |||
| c4e0dc3f5e | |||
| 22ed3a3565 | |||
| 80853d48f5 | |||
| d90667777f | |||
| 7990edd345 | |||
| 648804e5dc | |||
| 187e75de4d | |||
| e5df7ab3ad | |||
| 0465f9b188 | |||
| d53ad3b79a | |||
| 698bea4ec8 | |||
| 95039410b5 | |||
| e933fb11e9 | |||
| 0666d98648 | |||
| e314ae6df0 | |||
| bcb0978e9e | |||
| f3f892f448 | |||
| 1af1bc3460 | |||
| d0f99cee1a | |||
| f8b40085cd | |||
| 5de0714256 | |||
| bfa83cbf99 | |||
| 27ec6dbd7d | |||
| a8daee60fb | |||
| fab423cca0 | |||
| 70b69ecd19 | |||
| fe0ef8b579 | |||
| 586a04f812 | |||
| 293034bcde | |||
| ac516a4824 | |||
| 65e2fddfb5 | |||
| 2d167ae9f9 | |||
| 32779f596f | |||
| 273b96bfe0 | |||
| 4ffc073153 | |||
| fc841beedd | |||
| 9f4723f8eb | |||
| b37bb43e09 | |||
| 7a03291442 | |||
| 14f70351c0 | |||
| d195ce6340 | |||
| e9480ba1da | |||
| 1bd5d57884 | |||
| 6544e413b8 | |||
| d7437f125b | |||
| f2849ca00f | |||
| aa3dc574a7 | |||
| ba65556eac | |||
| 2bc5ad53c2 | |||
| bc317cfcd1 | |||
| 00a64710c2 | |||
| ba614af82d | |||
| 2b8342fcd5 | |||
| a75256bed5 | |||
| 0cc369a76e | |||
| 62ef8ed57a | |||
| 62188b49f0 | |||
| 305d5ad7b6 | |||
| d8c82829c4 | |||
| e3eaaea15a | |||
| 500c60b807 | |||
| ef53bf14ae | |||
| 25f9ca2eeb | |||
| d566dea265 | |||
| c81b61462e | |||
| 1fe362ecd0 | |||
| 257e8c4761 | |||
| a1537f4b0d | |||
| eeaab72a9f | |||
| 14e25ab9fe | |||
| 120aabfbce | |||
| 5e6c054345 | |||
| ed35555d74 | |||
| 619584b5f6 | |||
| 4cc99881d8 | |||
| 95ee1c423b | |||
| ff204d67b9 | |||
| 5ac3489ee5 | |||
| 9fd2dfa680 | |||
| 7650c81ad9 | |||
| c60b3f52a5 | |||
| 23d2cfe0f9 | |||
| 5dc44453ba | |||
| 6086899b28 | |||
| 2842017b59 | |||
| cc44d14722 | |||
| 54eb500315 | |||
| 0fe347c87a | |||
| fd69012357 | |||
| 9f62704c67 | |||
| 52cf236699 | |||
| e4285c2cba | |||
| 7b541af253 | |||
| 09e9b0361d | |||
| d0377d5d9d | |||
| fe21f6a8c4 | |||
| 6e85848bd0 | |||
| 2002d60fbe | |||
| 5774323f2e | |||
| 1350f84ea4 | |||
| 6dd090a848 | |||
| fccc25f7a3 | |||
| 67eb574fd4 | |||
| a69b161725 | |||
| 35fe8a23cb | |||
| a96702757e | |||
| f98fc1e03a | |||
| 39d752ca85 | |||
| a7e08054da | |||
| e0e8a99abe | |||
| c0e954f19d | |||
| 54cb8fa028 | |||
| ace2699e79 | |||
| 59c003bdce | |||
| 736d32c392 | |||
| add2f8ac80 | |||
| 22bbeef41a | |||
| 192f48b7f2 | |||
| dfcae03f25 |
@@ -7,6 +7,10 @@
|
||||
"forwardPorts": [
|
||||
8080
|
||||
],
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
@@ -17,7 +21,10 @@
|
||||
"extensions": [
|
||||
"ms-azuretools.vscode-docker",
|
||||
"golang.go",
|
||||
"rangav.vscode-thunder-client"
|
||||
"rangav.vscode-thunder-client",
|
||||
"GitHub.codespaces",
|
||||
"GitHub.copilot",
|
||||
"GitHub.copilot-chat"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/go
|
||||
image: mcr.microsoft.com/devcontainers/go:1.22
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
command: sleep infinity
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
apprise:
|
||||
image: caronc/apprise:latest
|
||||
image: caronc/apprise:1.0
|
||||
restart: unless-stopped
|
||||
hostname: apprise
|
||||
volumes:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Improvement
|
||||
about: Do you have an idea to improve an existing feature?
|
||||
title: ''
|
||||
labels: improvements
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
paths-ignore:
|
||||
- ./http/client/testdata
|
||||
@@ -1,4 +1,7 @@
|
||||
Do you follow the guidelines?
|
||||
|
||||
- [ ] I have tested my changes
|
||||
- [ ] There is no breaking changes
|
||||
- [ ] I really tested my changes and there is no regression
|
||||
- [ ] Ideally, my commit messages use the same convention as the Go project: https://go.dev/doc/contribute#commit_messages
|
||||
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Build Binaries
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
check-latest: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Compile binaries
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: make build
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: miniflux-*
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
@@ -29,14 +29,15 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/config.yml
|
||||
go-version: "1.22.x"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Debian Packages
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
@@ -16,9 +17,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
@@ -28,8 +29,34 @@ jobs:
|
||||
run: make debian-packages
|
||||
- name: List generated files
|
||||
run: ls -l *.deb
|
||||
build-packages-manually:
|
||||
if: github.event_name != 'pull_request' && github.event_name != 'push'
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
- name: Available Docker Platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Build Debian Packages
|
||||
run: make debian-packages
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages
|
||||
path: "*.deb"
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish-packages:
|
||||
if: ${{ ! github.event.pull_request }}
|
||||
if: github.event_name == 'push'
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -37,9 +64,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Build Alpine image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/alpine/Dockerfile
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Test Alpine Docker image
|
||||
run: docker run --rm ${{ github.repository_owner }}/miniflux:alpine-dev miniflux -i
|
||||
- name: Build Distroless image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/distroless/Dockerfile
|
||||
@@ -75,33 +75,33 @@ jobs:
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Quay Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_TOKEN }}
|
||||
|
||||
- name: Build and Push Alpine images
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/alpine/Dockerfile
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
tags: ${{ steps.docker_alpine_tag.outputs.tags }}
|
||||
|
||||
- name: Build and Push Distroless images
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/distroless/Dockerfile
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
jshint:
|
||||
@@ -12,20 +13,27 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install jshint
|
||||
- name: Install linters
|
||||
run: |
|
||||
sudo npm install -g jshint@2.13.3
|
||||
sudo npm install -g jshint@2.13.6 eslint@8.57.0
|
||||
- name: Run jshint
|
||||
run: jshint ui/static/js/*.js
|
||||
run: jshint internal/ui/static/js/*.js
|
||||
- name: Run ESLint
|
||||
run: eslint internal/ui/static/js/*.js
|
||||
|
||||
golangci:
|
||||
name: Golang Linter
|
||||
name: Golang Linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- uses: golangci/golangci-lint-action@v3
|
||||
go-version: "1.22.x"
|
||||
- run: "go vet ./..."
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace --enable gocritic
|
||||
- uses: dominikh/staticcheck-action@v1.3.1
|
||||
with:
|
||||
version: "2023.1.7"
|
||||
install-go: false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: RPM Packages
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
@@ -19,8 +20,25 @@ jobs:
|
||||
run: make rpm
|
||||
- name: List generated files
|
||||
run: ls -l *.rpm
|
||||
build-package-manually:
|
||||
if: github.event_name != 'pull_request' && github.event_name != 'push'
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build RPM Package
|
||||
run: make rpm
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages
|
||||
path: "*.rpm"
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish-package:
|
||||
if: ${{ ! github.event.pull_request }}
|
||||
if: github.event_name == 'push'
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
@@ -14,10 +15,10 @@ jobs:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
go-version: ["1.21"]
|
||||
go-version: ["1.22.x"]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout
|
||||
@@ -40,9 +41,9 @@ jobs:
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22.x"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Postgres client
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
miniflux-*
|
||||
miniflux
|
||||
./miniflux
|
||||
*.rpm
|
||||
*.deb
|
||||
.idea
|
||||
|
||||
@@ -1,3 +1,341 @@
|
||||
Version 2.1.2 (March 30, 2024)
|
||||
------------------------------
|
||||
|
||||
* `api`: rewrite API integration tests without build tags
|
||||
* `ci`: add basic ESLinter checks
|
||||
* `ci`: enable go-critic linter and fix various issues detected
|
||||
* `ci`: fix JavaScript linter path in GitHub Actions
|
||||
* `cli`: avoid misleading error message when creating an admin user automatically
|
||||
* `config`: add `FILTER_ENTRY_MAX_AGE_DAYS` option
|
||||
* `config`: bump the number of simultaneous workers
|
||||
* `config`: rename `PROXY_*` options to `MEDIA_PROXY_*`
|
||||
* `config`: use `crypto.GenerateRandomBytes` instead of doing it by hand
|
||||
* `http/request`: refactor conditions to be more idiomatic
|
||||
* `http/response`: remove legacy `X-XSS-Protection` header
|
||||
* `integration/rssbrige`: fix rssbrige import
|
||||
* `integration/shaarli`: factorize the header+payload concatenation as data
|
||||
* `integration/shaarli`: no need to base64-encode then remove the padding when we can simply encode without padding
|
||||
* `integration/shaarli`: the JWT token was declared as using HS256 as algorithm, but was using HS512
|
||||
* `integration/webhook`: add category title to request body
|
||||
* `locale`: update Turkish translations
|
||||
* `man page`: sort config options in alphabetical order
|
||||
* `mediaproxy`: reduce the internal indentation of `ProxifiedUrl` by inverting some conditions
|
||||
* `mediaproxy`: simplify and refactor the package
|
||||
* `model`: replace` Optional{Int,Int64,Float64}` with a generic function `OptionalNumber()`
|
||||
* `model`: use struct embedding for `FeedCreationRequestFromSubscriptionDiscovery` to reduce code duplication
|
||||
* `reader/atom`: avoid debug message when the date is empty
|
||||
* `reader/atom`: change `if !a { a = } if !a {a = }` constructs into `if !a { a = ; if !a {a = }}` to reduce the number of comparisons and improve readability
|
||||
* `reader/atom`: Move the population of the feed's entries into a new function, to make BuildFeed easier to understand/separate concerns/implementation details
|
||||
* `reader/atom`: refactor Atom parser to use an adapter
|
||||
* `reader/atom`: use `sort+compact` instead of `compact+sort` to remove duplicates
|
||||
* `reader/atom`: when detecting the format, detect its version as well
|
||||
* `reader/encoding`: inline a one-liner function
|
||||
* `reader/handler`: fix force refresh feature
|
||||
* `reader/json`: refactor JSON Feed parser to use an adapter
|
||||
* `reader/media`: remove a superfluous error-check: `strconv.ParseInt` returns `0` when passed an empty string
|
||||
* `reader/media`: simplify switch-case by moving a common condition above it
|
||||
* `reader/processor`: compile block/keep regex only once per feed
|
||||
* `reader/rdf`: refactor RDF parser to use an adapter
|
||||
* `reader/rewrite`: inline some one-line functions
|
||||
* `reader/rewrite`: simplify `removeClickbait`
|
||||
* `reader/rewrite`: transform a free-standing function into a method
|
||||
* `reader/rewrite`: use a proper constant instead of a magic number in `applyFuncOnTextContent`
|
||||
* `reader/rss`: add support for `<media:category>` element
|
||||
* `reader/rss`: don't add empty tags to RSS items
|
||||
* `reader/rss`: refactor RSS parser to use a default namespace to avoid some limitations of the Go XML parser
|
||||
* `reader/rss`: refactor RSS Parser to use an adapter
|
||||
* `reader/rss`: remove some duplicated code in RSS parser
|
||||
* `reader`: ensure that enclosure URLs are always absolute
|
||||
* `reader`: move iTunes and GooglePlay XML definitions to their own packages
|
||||
* `reader`: parse podcast categories
|
||||
* `reader`: remove trailing space in `SiteURL` and `FeedURL`
|
||||
* `storage`: do not store empty tags
|
||||
* `storage`: simplify `removeDuplicates()` to use a `sort`+`compact` construct instead of doing it by hand with a hashmap
|
||||
* `storage`: Use plain strings concatenation instead of building an array and then joining it
|
||||
* `timezone`: make sure the tests pass when the timezone database is not installed on the host
|
||||
* `ui/css`: align `min-width` with the other `min-width` values
|
||||
* `ui/css`: fix regression: "Add to Home Screen" button is unreadable
|
||||
* `ui/js`: don't use lambdas to return a function, use directly the function instead
|
||||
* `ui/js`: enable trusted-types
|
||||
* `ui/js`: fix download button loading label
|
||||
* `ui/js`: fix JavaScript error on the login page when the user not authenticated
|
||||
* `ui/js`: inline one-line functions
|
||||
* `ui/js`: inline some `querySelectorAll` calls
|
||||
* `ui/js`: reduce the scope of some variables
|
||||
* `ui/js`: remove a hack for "Chrome 67 and earlier" since it was released in 2018
|
||||
* `ui/js`: replace `DomHelper.findParent` with `.closest`
|
||||
* `ui/js`: replace `let` with `const`
|
||||
* `ui/js`: simplify `DomHelper.getVisibleElements` by using a `filter` instead of a loop with an index
|
||||
* `ui/js`: use a `Set` instead of an array in a `KeyboardHandler`'s member
|
||||
* `ui/js`: use some ternaries where it makes sense
|
||||
* `ui/static`: make use of `HashFromBytes` everywhere
|
||||
* `ui/static`: set minifier ECMAScript version
|
||||
* `ui`: add keyboard shortcuts for scrolling to top/bottom of the item list
|
||||
* `ui`: add media player control playback speed
|
||||
* `ui`: remove unused variables and improve JSON decoding in `saveEnclosureProgression()`
|
||||
* `validator`: display an error message on edit feed page when the feed URL is not unique
|
||||
* Bump `github.com/coreos/go-oidc/v3` from `3.9.0` to `3.10.0`
|
||||
* Bump `github.com/go-webauthn/webauthn` from `0.10.1` to `0.10.2`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.18` to `2.20.19`
|
||||
* Bump `google.golang.org/protobuf` from `1.32.0` to `1.33.0`
|
||||
|
||||
Version 2.1.1 (March 10, 2024)
|
||||
-----------------------------
|
||||
|
||||
* Move search form to a dedicated page
|
||||
* Add Readeck integration
|
||||
* Add feed option to disable HTTP/2 to avoid fingerprinting
|
||||
* Add `Enter` key as a hotkey to open selected item
|
||||
* Proxify `video` element `poster` attribute
|
||||
* Add a couple of new possible locations for feeds
|
||||
* Hugo likes to generate `index.xml`
|
||||
* `feed.atom` and `feed.rss` are used by enterprise-scale/old-school gigantic CMS
|
||||
* Fix categories import from Thunderbird's OPML
|
||||
* Fix logo misalignment when using languages that are more verbose than English
|
||||
* Google Reader: Do not return a 500 error when no items is returned
|
||||
* Handle RDF feeds with duplicated `<title>` elements
|
||||
* Sort integrations alphabetically
|
||||
* Add more URL validation in media proxy
|
||||
* Add unit test to ensure each translation has the correct number of plurals
|
||||
* Add missing plurals for some languages
|
||||
* Makefile: quiet `git describe` and `rev-parse` stderr: When building from a tarball instead of a cloned git repo, there would be two `fatal: not a git repository` errors emitted even though the build succeeds. This is because of how `VERSION` and `COMMIT` are set in the Makefile. This PR suppresses the stderr for these variable assignments.
|
||||
* Makefile: do not force `CGO_ENABLED=0` for `miniflux` target
|
||||
* Add GitHub Action pipeline to build packages on-demand
|
||||
* Remove Golint (deprecated), use `staticcheck` and `golangci-lint` instead
|
||||
* Build amd64/arm64 Debian packages with CGO disabled
|
||||
* Update `go.mod` and add `.exe` suffix to Windows binary
|
||||
* Add a couple of fuzzers
|
||||
* Fix CodeQL workflow
|
||||
* Code and performance improvements:
|
||||
* Use an `io.ReadSeeker` instead of an `io.Reader` to parse feeds
|
||||
* Speed up the sanitizer:
|
||||
- Allow Youtube URLs to start with `www`
|
||||
- Use `strings.Builder` instead of a `bytes.Buffer`
|
||||
- Use a `strings.NewReader` instead of a `bytes.NewBufferString`
|
||||
- Sprinkles a couple of `continue` to make the code-flow more obvious
|
||||
- Inline calls to `inList`, and put their parameters in the right order
|
||||
- Simplify `isPixelTracker`
|
||||
- Simplify `isValidIframeSource`, by extracting the hostname and comparing it directly, instead of using the full url and checking if it starts with multiple variations of the same one (`//`, `http:`, `https://` multiplied by `/www.`)
|
||||
- Add a benchmark
|
||||
- Instead of having to allocate a ~100 keys map containing possibly dynamic values (at least to the go compiler), allocate it once in a global variable. This significantly speeds things up, by reducing the garbage
|
||||
- Use constant time access for maps instead of iterating on them
|
||||
- Build a ~large whitelist map inline instead of constructing it item by item (and remove a duplicate key/value pair)
|
||||
- Use `slices` instead of hand-rolled loops
|
||||
collector/allocator involvements.
|
||||
* Reuse a `Reader` instead of copying to a buffer when parsing an Atom feed
|
||||
* Preallocate memory when exporting to OPML: This should marginally increase performance when exporting a large amount of feeds to OPML
|
||||
* Delay call of `view.New` after logging the user in: There is no need to do extra work like creating a session and its associated view until the user has been properly identified and as many possibly-failing sql request have been successfully run
|
||||
* Use constant-time comparison for anti-csrf tokens: This is probably completely overkill, but since anti-csrf tokens are secrets, they should be compared against untrusted inputs in constant time
|
||||
* Simplify and optimize `genericProxyRewriter`
|
||||
- Reduce the amount of nested loops: it's preferable to search the whole page once and filter on it (even with filters that should always be false), than searching it again for every element we're looking for.
|
||||
- Factorize the proxying conditions into a `shouldProxy` function to reduce the copy-pasta.
|
||||
* Speed up `removeUnlikelyCandidates`: `.Not` returns a brand new `Selection`, copied element by element
|
||||
* Improve `EstimateReadingTime`'s speed by a factor 7
|
||||
- Refactorise the tests and add some
|
||||
- Use 250 signs instead of the whole text
|
||||
- Only check for Korean, Chinese and Japanese script
|
||||
- Add a benchmark
|
||||
- Use a more idiomatic control flow
|
||||
* Don't compute reading-time when unused: If the user doesn't display reading times, there is no need to compute them. This should speed things up a bit, since `whatlanggo.Detect` is abysmally slow.
|
||||
* Simplify `username` generation for the integration tests: No need to generate random numbers 10 times, generate a single big-enough one. A single int64 should be more than enough
|
||||
* Add missing regex anchor detected by CodeQL
|
||||
* Don't mix up slices capacity and length
|
||||
* Use prepared statements for intervals, `ArchiveEntries` and `updateEnclosures`
|
||||
* Use modern for-loops introduced with Go 1.22
|
||||
* Remove a superfluous condition: No need to check if the length of `line` is positive since we're checking afterwards that it contains the `=` sign
|
||||
* Close resources as soon as possible, instead of using `defer()` in a loop
|
||||
* Remove superfluous escaping in a regex
|
||||
* Use `strings.ReplaceAll` instead of `strings.Replace(…, -1)`
|
||||
* Use `strings.EqualFold` instead of `strings.ToLower(…) ==`
|
||||
* Use `.WriteString(` instead of `.Write([]byte(…`
|
||||
* Use `%q` instead of `"%s"`
|
||||
* Make `internal/worker/worker.go` read-only
|
||||
* Use a switch-case construct in `internal/locale/plural.go` instead of an avalanche of `if`
|
||||
* Template functions: simplify `formatFileSize` and `duration` implementation
|
||||
* Inline some templating functions
|
||||
* Make use of `printer.Print` when possible
|
||||
* Add a `printer.Print` to `internal/locale/printer.go`: No need to use variadic functions with string format interpolation to generate static strings
|
||||
* Minor code simplification in `internal/ui/view/view.go`: No need to create the map item by item when we can create it in one go
|
||||
* Build the map inline in `CountAllFeeds()`: No need to build an empty map to then add more fields in it one by one
|
||||
* Miscellaneous improvements to `internal/reader/subscription/finder.go`:
|
||||
- Surface `localizedError` in `FindSubscriptionsFromWellKnownURLs` via `slog`
|
||||
- Use an inline declaration for new subscriptions, like done elsewhere in the
|
||||
file, if only for consistency's sake
|
||||
- Preallocate the `subscriptions` slice when using an RSS-bridge,
|
||||
* Use an update-where for `MarkCategoryAsRead` instead of a subquery
|
||||
* Simplify `CleanOldUserSessions`' query: No need for a subquery, filtering on `created_at` directly is enough
|
||||
* Simplify `cleanupEntries`' query
|
||||
- `NOT (hash=ANY(%4))` can be expressed as `hash NOT IN $4`
|
||||
- There is no need for a subquery operating on the same table, moving the conditions out is equivalent.
|
||||
* Reformat `ArchiveEntries`'s query for consistency's sake and replace the `=ANY` with an `IN`
|
||||
* Reformat the query in `GetEntryIDs` and `GetReadTime`'s query for consistency's sake
|
||||
* Simplify `WeeklyFeedEntryCount`: No need for a `BETWEEN`: we want to filter on entries published in the last week, no need to express is as "entries published between now and last week", "entries published after last week" is enough
|
||||
* Add some tests for `add_image_title`
|
||||
* Remove `github.com/google/uuid` dependencies: Replace it with a hand-rolled implementation. Heck, an UUID isn't even a requirement according to Omnivore API docs
|
||||
* Simplify `internal/reader/icon/finder.go`:
|
||||
- Use a simple regex to parse data uri instead of a hand-rolled parser, and document what fields are considered mandatory.
|
||||
- Use case-insensitive matching to find (fav)icons, instead of doing the same query twice with different letter cases
|
||||
- Add `apple-touch-icon-precomposed.png` as a fallback `favicon`
|
||||
- Reorder the queries to have `icon` first, since it seems to be the most popular one. It used to be last, meaning that pages had to be parsed completely 4 times, instead of one now.
|
||||
- Minor factorisation in `findIconURLsFromHTMLDocument`
|
||||
* Small refactoring of `internal/reader/date/parser.go`:
|
||||
- Split dates formats into those that require local times and those who don't, so that there is no need to have a switch-case in the for loop with around 250 iterations at most.
|
||||
- Be more strict when it comes to timezones, previously invalid ones like -13 were accepted. Also add a test for this.
|
||||
- Bail out early if the date is an empty string.
|
||||
* Make use of Go ≥ 1.21 slices package instead of hand-rolled loops
|
||||
* Reorder the fields of the `Entry` struct to save some memory
|
||||
* Dependencies update:
|
||||
* Bump `golang.org/x/oauth2` from `0.17.0` to `0.18.0`
|
||||
* Bump `github.com/prometheus/client_golang` from `1.18.0` to `1.19.0`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.16` to `2.20.18`
|
||||
* Bump `github.com/PuerkitoBio/goquery` from `1.8.1` to `1.9.1`
|
||||
* Bump `golang.org/x/crypto` from `0.19.0` to `0.20.0`
|
||||
* Bump `github.com/go-jose/go-jose/v3` from `3.0.1` to `3.0.3`
|
||||
|
||||
Version 2.1.0 (February 17, 2024)
|
||||
---------------------------------
|
||||
|
||||
* Add Linkwarden integration
|
||||
* Add LinkAce integration
|
||||
* Add `FORCE_REFRESH_INTERVAL` config option
|
||||
* Add `item-meta-info-reading-time` CSS class
|
||||
* Add `add_dynamic_iframe` rewrite function
|
||||
* Add attribute `data-original-mos` to `add_dynamic_image` rewrite candidates
|
||||
* Update entry processor to allow blocking/keeping entries by tags and/or authors
|
||||
* Change default `Accept` header when fetching feeds
|
||||
* Rewrite relative RSS Bridge URL to absolute URL
|
||||
* Use numeric user ID in Alpine and distroless container image (avoid `securityContext` error in Kubernetes)
|
||||
* Always try to use HTTP/2 when fetching feeds if available
|
||||
* Add `type` attribute in OPML export as per OPML 2.0 specs
|
||||
* Fix missing translation argument for the key `error.unable_to_parse_feed`
|
||||
* Fix Debian package builder when using Go 1.22 and `armhf` architecture
|
||||
* Fix typo in log message
|
||||
* Fix incorrect label shown when saving an article
|
||||
* Fix incorrect condition in refresh feeds cli
|
||||
* Fix incorrect label `for` attribute
|
||||
* Add missing label ID for custom CSS field
|
||||
* Accessibility improvements:
|
||||
* Add workaround for macOS VoiceOver that didn't announce `details` and `summary` when expanded
|
||||
* Add `alert` role to alert message element
|
||||
* Add a `h2` heading to the article element so that the screen reader users can navigate the article through the heading level
|
||||
* Add an `aria-label` attribute for the article element for screen readers
|
||||
* Remove the icon image `alt` attribute in feeds list to prevent screen reader to announce it before entry title
|
||||
* Add `sr-only` CSS class for screen reader users (provides more context)
|
||||
* Differentiate between buttons and links
|
||||
* Change links that could perform actions to buttons
|
||||
* Improve translation of hidden Aria elements
|
||||
* Remove the redundant article role
|
||||
* Add a search landmark for the search form so that the screen reader users can navigate to it
|
||||
* Add skip to content link
|
||||
* Add `nav` landmark to page header links
|
||||
* Limit feed/category entry pagination to unread entries when coming from unread entry list
|
||||
* Update German translation
|
||||
* Update GitHub Actions to Go 1.22
|
||||
* Bump `golang.org/x/term` from `0.16.0` to `0.17.0`
|
||||
* Bump `github.com/google/uuid` from `1.5.0` to `1.6.0`
|
||||
* Bump `github.com/yuin/goldmark` from `1.6.0` to `1.7.0`
|
||||
* Bump `golang.org/x/oauth2` from `0.15.0` to `0.17.0`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.10` to `2.20.12`
|
||||
* Bump `golang.org/x/term` from `0.15.0` to `0.16.0`
|
||||
* Bump `github.com/prometheus/client_golang` from `1.17.0` to `1.18.0`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.9` to `2.20.16`
|
||||
* Bump `golang.org/x/crypto` from `0.16.0` to `0.19.0`
|
||||
* Bump `github.com/go-webauthn/webauthn` from `0.9.4` to` 0.10.1`
|
||||
* Bump `golang.org/x/net` from `0.20.0` to `0.21.0`
|
||||
|
||||
Version 2.0.51 (December 13, 2023)
|
||||
----------------------------------
|
||||
|
||||
* Add Omnivore integration
|
||||
* Fixes for the regressions introduced in version 2.0.50:
|
||||
* Ensure all HTML documents are encoded in UTF-8
|
||||
* Send default User-Agent and HTTP caching headers when making HTTP requests
|
||||
* Allow Youtube links to be opened outside the `iframe` (avoid `ERR_BLOCKED_BY_RESPONSE` error)
|
||||
* Fix inaccessible metrics endpoint when listening on Unix socket
|
||||
* Allow renaming and moving feed at the same time in the Google Reader API
|
||||
* Log `nb_jobs` only when number of jobs is larger than 0 in background scheduler
|
||||
* Deduplicate feed URLs when parsing HTML document during discovery process
|
||||
* Calculate a virtual weekly count based on the average updating frequency (`POLLING_SCHEDULER=entry_frequency`)
|
||||
* Update GitHub Actions workflow to be able to run the linter and tests on-demand
|
||||
* Add `SCHEDULER_ROUND_ROBIN_MIN_INTERVAL` config option
|
||||
* Add links to GitHub for the commit hash and the version in the about page
|
||||
* Use "starred" rather than "bookmarked" in English translation
|
||||
* Update Chinese (CN & TW) translation
|
||||
* Bump `github.com/google/uuid` from `1.4.0` to `1.5.0`
|
||||
* Bump `github.com/coreos/go-oidc/v3` from `3.7.0` to `3.9.0`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.6` to `2.20.9`
|
||||
* Bump `github.com/go-webauthn/webauthn` from `0.8.6` to `0.9.4`
|
||||
* Bump `golang.org/x/oauth2` from `0.14.0` to `0.15.0`
|
||||
|
||||
Version 2.0.50 (November 12, 2023)
|
||||
----------------------------------
|
||||
|
||||
* Add WebAuthn / Passkey integration
|
||||
* Add RSS-Bridge integration
|
||||
* Take RSS TTL field into consideration to schedule next check date
|
||||
* Show number of visible entries instead of number of read entries in feed list
|
||||
* OpenID Connect: Redirect to configured user home page after successful authentication
|
||||
* Google Reader API fixes:
|
||||
* `user/{userID}/state/com.google/read` is missing in categories section for read entries
|
||||
* Take `ExcludeTargets` into consideration in feed stream handler
|
||||
* Allow iframes pointing to Twitch videos
|
||||
* Filter feed entries based on URL or title
|
||||
* Take into consideration `hide_globally` property defined for categories in `/v1/entries` API endpoint
|
||||
* Add category ID to webhooks request body
|
||||
* Update date parser to parse more invalid date formats
|
||||
* Refactor feed discovery handler, and avoid an extra HTTP request if the URL provided is the feed
|
||||
* Refactor HTTP Client and `LocalizedError` packages
|
||||
* Refactor Batch Builder, and prevent accidental and excessive refreshes from the web UI
|
||||
* Refactor icon finder:
|
||||
- Continue the discovery process when the feed icon is invalid
|
||||
- Search all icons from the HTML document and do not stop on the first one
|
||||
* Add support for SVG icons with data URL without encoding
|
||||
* Expose `next_check_at` in the web ui and API
|
||||
* Add database indexes to improve performance
|
||||
* Change log level to warning for failed feeds refresh in cronjob
|
||||
* Do not log website without icon as warning
|
||||
* Add GitHub workflow to build binaries
|
||||
* Add GitHub extensions to devcontainer
|
||||
* Make sure to pull the latest base image when building the Docker image
|
||||
* Strip version prefix when building Debian package
|
||||
* Add `github-cli` and `docker-outside-of-docker` features to devcontainer
|
||||
* Bump `golang.org/x/*` dependencies
|
||||
* Bump `github.com/gorilla/mux` from `1.8.0` to `1.8.1`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.19.9` to `2.20.6`
|
||||
* Bump `github.com/yuin/goldmark` from `1.5.6` to `1.6.0`
|
||||
* Bump `github.com/coreos/go-oidc/v3` from `3.6.0` to `3.7.0`
|
||||
|
||||
Version 2.0.49 (October 15, 2023)
|
||||
---------------------------------
|
||||
|
||||
* Implement structured logging using `log/slog` package. New config options available:
|
||||
* `LOG_FORMAT`: `json` or `text`
|
||||
* `LOG_LEVEL`: `debug`, `info`, `warning`, or `error`
|
||||
* `LOG_FILE`: `sdterr`, `stdout`, or a file path
|
||||
* The `DEBUG` option is now deprecated in favor of `LOG_LEVEL`
|
||||
* API Improvements:
|
||||
* Add endpoint `/v1/version`
|
||||
* Add endpoint `PUT /v1/entries` to update entry title and content
|
||||
* Add endpoint `/v1/icons/{iconID}`
|
||||
* Add endpoint `/v1/flush-history` to flush history
|
||||
* Make the category optional when creating feeds for API clients who don't support categories
|
||||
* Add enclosures to `GET /v1/entries` endpoint
|
||||
* Add `published_after`, `published_before`, `changed_after` and `changed_before` options to `/v1/entries` endpoint
|
||||
* Telegram integration improvements:
|
||||
* Replace feed HTML link with a button to avoid page preview issues
|
||||
* Add the possibility to disable buttons
|
||||
* Add Bruno Miniflux API collection in `contrib` folder (Bruno is an open source alternative to Postman/Insomnia)
|
||||
* Add command line argument to export user feeds as OPML
|
||||
* Add new rewrite rules `add_hn_links_using_hack` and `add_hn_links_using_opener` to open HN comments with iOS apps
|
||||
* Fix timestamp format for `Expires` response header
|
||||
* Fix Javascript error when reading time option is disabled
|
||||
* Fix Apprise logic to handle feed service URLs
|
||||
* Fix missing word in force refresh message
|
||||
* Remove deprecated `PreferServerCipherSuites` TLS option
|
||||
* Replace `github.com/rylans/getlang` with `github.com/abadojack/whatlanggo` because `getlang` doesn't seems to be updated anymore
|
||||
* Bump `github.com/mccutchen/go-httpbin/v2` from `2.11.0` to `2.11.1`
|
||||
* Bump `golang.org/x/*` dependencies
|
||||
|
||||
Version 2.0.48 (September 15, 2023)
|
||||
-----------------------------------
|
||||
|
||||
@@ -65,7 +403,7 @@ Version 2.0.47 (August 20, 2023)
|
||||
* Add new API endpoint: `/entries/{entryID}/save`
|
||||
* Trigger Docker and packages workflows only for semantic tags
|
||||
* Go module versioning expect Git tags to start with the letter v.
|
||||
* The goal is to keep the existing naming convention for generated artifacts and
|
||||
* The goal is to keep the existing naming convention for generated artifacts and
|
||||
have proper versioning for the Go module.
|
||||
* Bump `golang.org/x/*` dependencies
|
||||
* Bump `github.com/yuin/goldmark`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/miniflux
|
||||
VERSION := $(shell git describe --tags --abbrev=0)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
|
||||
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
@@ -12,6 +12,7 @@ export PGPASSWORD := postgres
|
||||
|
||||
.PHONY: \
|
||||
miniflux \
|
||||
miniflux-no-pie \
|
||||
linux-amd64 \
|
||||
linux-arm64 \
|
||||
linux-armv7 \
|
||||
@@ -43,7 +44,10 @@ export PGPASSWORD := postgres
|
||||
debian-packages
|
||||
|
||||
miniflux:
|
||||
@ CGO_ENABLED=0 go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
@ go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
miniflux-no-pie:
|
||||
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
linux-amd64:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@@ -73,7 +77,7 @@ openbsd-amd64:
|
||||
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
windows-amd64:
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||
|
||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
|
||||
@@ -94,19 +98,21 @@ openbsd-x86:
|
||||
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
windows-x86:
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||
|
||||
run:
|
||||
@ LOG_DATE_TIME=1 DEBUG=1 RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||
|
||||
clean:
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe
|
||||
|
||||
test:
|
||||
go test -cover -race -count=1 ./...
|
||||
|
||||
lint:
|
||||
golint -set_exit_status ${PKG_LIST}
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
golangci-lint run --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||
|
||||
integration-test:
|
||||
psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
@@ -120,9 +126,13 @@ integration-test:
|
||||
RUN_MIGRATIONS=1 \
|
||||
DEBUG=1 \
|
||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
|
||||
|
||||
while ! nc -z localhost 8080; do sleep 1; done
|
||||
go test -v -tags=integration -count=1 miniflux.app/v2/internal/tests
|
||||
|
||||
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
|
||||
TEST_MINIFLUX_ADMIN_USERNAME=admin \
|
||||
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
||||
go test -v -count=1 ./internal/api
|
||||
|
||||
clean-integration-test:
|
||||
@ kill -9 `cat /tmp/miniflux.pid`
|
||||
@@ -131,7 +141,7 @@ clean-integration-test:
|
||||
@ psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
|
||||
docker-image:
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/alpine/Dockerfile .
|
||||
docker build --pull -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/alpine/Dockerfile .
|
||||
|
||||
docker-image-distroless:
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/distroless/Dockerfile .
|
||||
|
||||
+127
-10
@@ -18,16 +18,60 @@ type Client struct {
|
||||
}
|
||||
|
||||
// New returns a new Miniflux client.
|
||||
// Deprecated: use NewClient instead.
|
||||
func New(endpoint string, credentials ...string) *Client {
|
||||
// Web gives "API Endpoint = https://miniflux.app/v1/", it doesn't work (/v1/v1/me)
|
||||
return NewClient(endpoint, credentials...)
|
||||
}
|
||||
|
||||
// NewClient returns a new Miniflux client.
|
||||
func NewClient(endpoint string, credentials ...string) *Client {
|
||||
// Trim trailing slashes and /v1 from the endpoint.
|
||||
endpoint = strings.TrimSuffix(endpoint, "/")
|
||||
endpoint = strings.TrimSuffix(endpoint, "/v1")
|
||||
// trim to https://miniflux.app
|
||||
|
||||
if len(credentials) == 2 {
|
||||
switch len(credentials) {
|
||||
case 2:
|
||||
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
||||
case 1:
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
default:
|
||||
return &Client{request: &request{endpoint: endpoint}}
|
||||
}
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
}
|
||||
|
||||
// Healthcheck checks if the application is up and running.
|
||||
func (c *Client) Healthcheck() error {
|
||||
body, err := c.request.Get("/healthcheck")
|
||||
if err != nil {
|
||||
return fmt.Errorf("miniflux: unable to perform healthcheck: %w", err)
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
responseBodyContent, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("miniflux: unable to read healthcheck response: %w", err)
|
||||
}
|
||||
|
||||
if string(responseBodyContent) != "OK" {
|
||||
return fmt.Errorf("miniflux: invalid healthcheck response: %q", responseBodyContent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version returns the version of the Miniflux instance.
|
||||
func (c *Client) Version() (*VersionResponse, error) {
|
||||
body, err := c.request.Get("/v1/version")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var versionResponse *VersionResponse
|
||||
if err := json.NewDecoder(body).Decode(&versionResponse); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: json error (%v)", err)
|
||||
}
|
||||
|
||||
return versionResponse, nil
|
||||
}
|
||||
|
||||
// Me returns the logged user information.
|
||||
@@ -484,6 +528,22 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateEntry updates an entry.
|
||||
func (c *Client) UpdateEntry(entryID int64, entryChanges *EntryModificationRequest) (*Entry, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d", entryID), entryChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var entry *Entry
|
||||
if err := json.NewDecoder(body).Decode(&entry); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// ToggleBookmark toggles entry bookmark value.
|
||||
func (c *Client) ToggleBookmark(entryID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||
@@ -496,7 +556,26 @@ func (c *Client) SaveEntry(entryID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchCounters
|
||||
// FetchEntryOriginalContent fetches the original content of an entry using the scraper.
|
||||
func (c *Client) FetchEntryOriginalContent(entryID int64) (string, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d/fetch-content", entryID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var response struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(body).Decode(&response); err != nil {
|
||||
return "", fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return response.Content, nil
|
||||
}
|
||||
|
||||
// FetchCounters fetches feed counters.
|
||||
func (c *Client) FetchCounters() (*FeedCounters, error) {
|
||||
body, err := c.request.Get("/v1/feeds/counters")
|
||||
if err != nil {
|
||||
@@ -512,6 +591,28 @@ func (c *Client) FetchCounters() (*FeedCounters, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// FlushHistory changes all entries with the status "read" to "removed".
|
||||
func (c *Client) FlushHistory() error {
|
||||
_, err := c.request.Put("/v1/flush-history", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Icon fetches a feed icon.
|
||||
func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/icons/%d", iconID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var feedIcon *FeedIcon
|
||||
if err := json.NewDecoder(body).Decode(&feedIcon); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return feedIcon, nil
|
||||
}
|
||||
|
||||
func buildFilterQueryString(path string, filter *Filter) string {
|
||||
if filter != nil {
|
||||
values := url.Values{}
|
||||
@@ -540,14 +641,30 @@ func buildFilterQueryString(path string, filter *Filter) string {
|
||||
values.Set("after", strconv.FormatInt(filter.After, 10))
|
||||
}
|
||||
|
||||
if filter.AfterEntryID > 0 {
|
||||
values.Set("after_entry_id", strconv.FormatInt(filter.AfterEntryID, 10))
|
||||
}
|
||||
|
||||
if filter.Before > 0 {
|
||||
values.Set("before", strconv.FormatInt(filter.Before, 10))
|
||||
}
|
||||
|
||||
if filter.PublishedAfter > 0 {
|
||||
values.Set("published_after", strconv.FormatInt(filter.PublishedAfter, 10))
|
||||
}
|
||||
|
||||
if filter.PublishedBefore > 0 {
|
||||
values.Set("published_before", strconv.FormatInt(filter.PublishedBefore, 10))
|
||||
}
|
||||
|
||||
if filter.ChangedAfter > 0 {
|
||||
values.Set("changed_after", strconv.FormatInt(filter.ChangedAfter, 10))
|
||||
}
|
||||
|
||||
if filter.ChangedBefore > 0 {
|
||||
values.Set("changed_before", strconv.FormatInt(filter.ChangedBefore, 10))
|
||||
}
|
||||
|
||||
if filter.AfterEntryID > 0 {
|
||||
values.Set("after_entry_id", strconv.FormatInt(filter.AfterEntryID, 10))
|
||||
}
|
||||
|
||||
if filter.BeforeEntryID > 0 {
|
||||
values.Set("before_entry_id", strconv.FormatInt(filter.BeforeEntryID, 10))
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ This code snippet fetch the list of users:
|
||||
miniflux "miniflux.app/v2/client"
|
||||
)
|
||||
|
||||
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
|
||||
users, err := client.Users()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
+77
-47
@@ -41,6 +41,7 @@ type User struct {
|
||||
DefaultHomePage string `json:"default_home_page"`
|
||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
@@ -58,28 +59,29 @@ type UserCreationRequest struct {
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
@@ -107,7 +109,7 @@ type Subscription struct {
|
||||
}
|
||||
|
||||
func (s Subscription) String() string {
|
||||
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
|
||||
return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
|
||||
}
|
||||
|
||||
// Subscriptions represents a list of subscriptions.
|
||||
@@ -140,6 +142,7 @@ type Feed struct {
|
||||
Password string `json:"password"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
@@ -160,6 +163,7 @@ type FeedCreationRequest struct {
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
@@ -182,6 +186,7 @@ type FeedModificationRequest struct {
|
||||
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
DisableHTTP2 *bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
@@ -202,24 +207,30 @@ type Feeds []*Feed
|
||||
// Entry represents a subscription item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Date time.Time `json:"published_at"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Starred bool `json:"starred"`
|
||||
}
|
||||
|
||||
// EntryModificationRequest represents a request to modify an entry.
|
||||
type EntryModificationRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Content *string `json:"content"`
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
@@ -245,20 +256,24 @@ const (
|
||||
|
||||
// Filter is used to filter entries.
|
||||
type Filter struct {
|
||||
Status string
|
||||
Offset int
|
||||
Limit int
|
||||
Order string
|
||||
Direction string
|
||||
Starred string
|
||||
Before int64
|
||||
After int64
|
||||
BeforeEntryID int64
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
Status string
|
||||
Offset int
|
||||
Limit int
|
||||
Order string
|
||||
Direction string
|
||||
Starred string
|
||||
Before int64
|
||||
After int64
|
||||
PublishedBefore int64
|
||||
PublishedAfter int64
|
||||
ChangedBefore int64
|
||||
ChangedAfter int64
|
||||
BeforeEntryID int64
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
@@ -266,3 +281,18 @@ type EntryResultSet struct {
|
||||
Total int `json:"total"`
|
||||
Entries Entries `json:"entries"`
|
||||
}
|
||||
|
||||
// VersionResponse represents the version and the build information of the Miniflux instance.
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"build_date"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Compiler string `json:"compiler"`
|
||||
Arch string `json:"arch"`
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
||||
func SetOptionalField[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ var (
|
||||
ErrForbidden = errors.New("miniflux: access forbidden")
|
||||
ErrServerError = errors.New("miniflux: internal server error")
|
||||
ErrNotFound = errors.New("miniflux: resource not found")
|
||||
ErrBadRequest = errors.New("miniflux: bad request")
|
||||
)
|
||||
|
||||
type errorResponse struct {
|
||||
@@ -124,10 +125,10 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
var resp errorResponse
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&resp); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: bad request error (%v)", err)
|
||||
return nil, fmt.Errorf("%w (%v)", ErrBadRequest, err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
|
||||
return nil, fmt.Errorf("%w (%s)", ErrBadRequest, resp.ErrorMessage)
|
||||
}
|
||||
|
||||
if response.StatusCode > 400 {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
This folder contains Miniflux API collection for [Bruno](https://www.usebruno.com).
|
||||
|
||||
Bruno is a lightweight alternative to Postman/Insomnia.
|
||||
|
||||
- https://www.usebruno.com
|
||||
- https://github.com/usebruno/bruno
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Bookmark an entry
|
||||
type: http
|
||||
seq: 37
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/bookmark
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
entryID: 1698
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Create a feed
|
||||
type: http
|
||||
seq: 19
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/feeds
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Create a new category
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/categories
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Create a new user
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/users
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"username": "foobar",
|
||||
"password": "secret123"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Delete a category
|
||||
type: http
|
||||
seq: 12
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 1
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Delete a feed
|
||||
type: http
|
||||
seq: 26
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 18
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Delete a user
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{minifluxBaseURL}}/v1/users/{{userID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"language": "fr_FR"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
userID: 2
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Discover feeds
|
||||
type: http
|
||||
seq: 18
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/discover
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"url": "https://miniflux.app"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Fetch entry website content
|
||||
type: http
|
||||
seq: 39
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/fetch-content
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
entryID: 1698
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Flush history
|
||||
type: http
|
||||
seq: 40
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/flush-history
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"url": "https://miniflux.app"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get a single entry
|
||||
type: http
|
||||
seq: 36
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
entryID: 1698
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
meta {
|
||||
name: Get a single feed entry
|
||||
type: http
|
||||
seq: 33
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries/{{entryID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
entryID: 1698
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get a single feed
|
||||
type: http
|
||||
seq: 24
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 18
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get a single user by ID
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/users/{{userID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
userID: 1
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get a single user by username
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/users/{{username}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
username: admin
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: Get all categories
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/categories
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Get all entries
|
||||
type: http
|
||||
seq: 34
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/entries
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Get all feeds
|
||||
type: http
|
||||
seq: 20
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: Get all users
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/users
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get category entries
|
||||
type: http
|
||||
seq: 16
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 2
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
meta {
|
||||
name: Get category entry
|
||||
type: http
|
||||
seq: 17
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries/{{entryID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 2
|
||||
entryID: 1
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get category feeds
|
||||
type: http
|
||||
seq: 14
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/feeds
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 2
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: Get current user
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/me
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Get feed counters
|
||||
type: http
|
||||
seq: 21
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/counters
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get feed entries
|
||||
type: http
|
||||
seq: 32
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get feed icon by feed ID
|
||||
type: http
|
||||
seq: 27
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/icon
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Get feed icon by icon ID
|
||||
type: http
|
||||
seq: 28
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/icons/{{iconID}}
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
iconID: 11
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: Get version and build information
|
||||
type: http
|
||||
seq: 42
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/version
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Mark all category entries as read
|
||||
type: http
|
||||
seq: 13
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/mark-all-as-read
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 2
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Mark all user entries as read
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/users/{{userID}}/mark-all-as-read
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
userID: 1
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Mark feed as read
|
||||
type: http
|
||||
seq: 29
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/mark-all-as-read
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: OPML Export
|
||||
type: http
|
||||
seq: 30
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{minifluxBaseURL}}/v1/export
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
meta {
|
||||
name: OPML Import
|
||||
type: http
|
||||
seq: 31
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/import
|
||||
body: xml
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
body:xml {
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="2.0">
|
||||
<head>
|
||||
<title>Miniflux</title>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="My category">
|
||||
<outline title="Miniflux" text="Miniflux" xmlUrl="https://miniflux.app/feed.xml" htmlUrl="https://miniflux.app"></outline>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 19
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Refresh a single feed
|
||||
type: http
|
||||
seq: 23
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/refresh
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 18
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Refresh all feeds
|
||||
type: http
|
||||
seq: 22
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/refresh
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Refresh category feeds
|
||||
type: http
|
||||
seq: 15
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/refresh
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 2
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Save an entry
|
||||
type: http
|
||||
seq: 38
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/save
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"feed_url": "https://miniflux.app/feed.xml"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
entryID: 1698
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Update a category
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Update"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
categoryID: 1
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Update a feed
|
||||
type: http
|
||||
seq: 25
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"user_agent": "My user agent"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
feedID: 18
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Update a user
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/users/{{userID}}
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"language": "fr_FR"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
userID: 1
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Update entries status
|
||||
type: http
|
||||
seq: 35
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/entries
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"entry_ids": [1698, 1699],
|
||||
"status": "read"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
meta {
|
||||
name: Update entry
|
||||
type: http
|
||||
seq: 41
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
|
||||
body: json
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: {{minifluxUsername}}
|
||||
password: {{minifluxPassword}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "New title",
|
||||
"content": "Some text"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
entryID: 1789
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Miniflux",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
vars {
|
||||
minifluxBaseURL: http://127.0.0.1:8080
|
||||
minifluxUsername: admin
|
||||
}
|
||||
vars:secret [
|
||||
minifluxPassword
|
||||
]
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
- POSTGRES_DB=miniflux
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
module miniflux.app/v2
|
||||
|
||||
// +heroku goVersion go1.21
|
||||
// +heroku goVersion go1.22
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/coreos/go-oidc/v3 v3.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/PuerkitoBio/goquery v1.9.1
|
||||
github.com/abadojack/whatlanggo v1.0.1
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mccutchen/go-httpbin/v2 v2.11.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0
|
||||
github.com/tdewolff/minify/v2 v2.12.8
|
||||
github.com/yuin/goldmark v1.5.6
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/oauth2 v0.11.0
|
||||
golang.org/x/term v0.11.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/tdewolff/minify/v2 v2.20.19
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.7 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
)
|
||||
|
||||
go 1.21
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
||||
go 1.22
|
||||
|
||||
@@ -1,117 +1,121 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
||||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
|
||||
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
|
||||
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
|
||||
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
|
||||
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
|
||||
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mccutchen/go-httpbin/v2 v2.11.0 h1:uDVmzefIbcG9NeEmWxwy7PdxztY+oCRlOLbHSaXdSF4=
|
||||
github.com/mccutchen/go-httpbin/v2 v2.11.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0 h1:qSaU9YAEIxk/ozcmY1hiauktAYTpbwYIrPdQ0L2E8UM=
|
||||
github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0=
|
||||
github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E=
|
||||
github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg=
|
||||
github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
|
||||
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
|
||||
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
|
||||
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
|
||||
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
|
||||
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
|
||||
github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||
|
||||
+20
-1
@@ -5,8 +5,11 @@ package api // import "miniflux.app/v2/internal/api"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/version"
|
||||
"miniflux.app/v2/internal/worker"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -54,7 +57,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/icon", handler.getIconByFeedID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
|
||||
@@ -63,7 +66,23 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries/{entryID}", handler.updateEntry).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/entries/{entryID}/save", handler.saveEntry).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
||||
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (h *handler) versionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.OK(w, r, &versionResponse{
|
||||
Version: version.Version,
|
||||
Commit: version.Commit,
|
||||
BuildDate: version.BuildDate,
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Arch: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,11 @@ package api // import "miniflux.app/v2/internal/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
@@ -135,15 +137,27 @@ func (h *handler) refreshCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
jobs, err := h.store.NewCategoryBatch(userID, categoryID, h.store.CountFeeds(userID))
|
||||
batchBuilder := h.store.NewBatchBuilder()
|
||||
batchBuilder.WithErrorLimit(config.Opts.PollingParsingErrorLimit())
|
||||
batchBuilder.WithoutDisabledFeeds()
|
||||
batchBuilder.WithUserID(userID)
|
||||
batchBuilder.WithCategoryID(categoryID)
|
||||
batchBuilder.WithNextCheckExpired()
|
||||
|
||||
jobs, err := batchBuilder.FetchJobs()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
h.pool.Push(jobs)
|
||||
}()
|
||||
slog.Info(
|
||||
"Triggered a manual refresh of all feeds for a given category from the API",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("category_id", categoryID),
|
||||
slog.Int("nb_jobs", len(jobs)),
|
||||
)
|
||||
|
||||
go h.pool.Push(jobs)
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
+93
-21
@@ -15,9 +15,10 @@ import (
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxy"
|
||||
"miniflux.app/v2/internal/reader/processor"
|
||||
"miniflux.app/v2/internal/reader/readingtime"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
@@ -35,14 +36,14 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b
|
||||
return
|
||||
}
|
||||
|
||||
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
|
||||
proxyOption := config.Opts.ProxyOption()
|
||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entry.Content)
|
||||
proxyOption := config.Opts.MediaProxyMode()
|
||||
|
||||
for i := range entry.Enclosures {
|
||||
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
|
||||
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
||||
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
|
||||
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||
entry.Enclosures[i].URL = mediaproxy.ProxifyAbsoluteURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -147,6 +148,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(limit)
|
||||
builder.WithTags(tags)
|
||||
builder.WithEnclosures()
|
||||
configureFilters(builder, r)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
@@ -162,7 +164,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
||||
}
|
||||
|
||||
for i := range entries {
|
||||
entries[i].Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entries[i].Content)
|
||||
entries[i].Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entries[i].Content)
|
||||
}
|
||||
|
||||
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
||||
@@ -231,6 +233,60 @@ func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
|
||||
json.Accepted(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) updateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
var entryUpdateRequest model.EntryUpdateRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&entryUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.ValidateEntryModification(&entryUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
loggedUserID := request.UserID(r)
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
|
||||
entryBuilder := h.store.NewEntryQueryBuilder(loggedUserID)
|
||||
entryBuilder.WithEntryID(entryID)
|
||||
entryBuilder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := entryBuilder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.store.UserByID(loggedUserID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
entryUpdateRequest.Patch(entry)
|
||||
if user.ShowReadingTime {
|
||||
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
|
||||
}
|
||||
|
||||
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.Created(w, r, entry)
|
||||
}
|
||||
|
||||
func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
loggedUserID := request.UserID(r)
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
@@ -250,7 +306,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.store.UserByID(entry.UserID)
|
||||
user, err := h.store.UserByID(loggedUserID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -282,29 +338,46 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
json.OK(w, r, map[string]string{"content": entry.Content})
|
||||
}
|
||||
|
||||
func (h *handler) flushHistory(w http.ResponseWriter, r *http.Request) {
|
||||
loggedUserID := request.UserID(r)
|
||||
go h.store.FlushHistory(loggedUserID)
|
||||
json.Accepted(w, r)
|
||||
}
|
||||
|
||||
func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) {
|
||||
beforeEntryID := request.QueryInt64Param(r, "before_entry_id", 0)
|
||||
if beforeEntryID > 0 {
|
||||
if beforeEntryID := request.QueryInt64Param(r, "before_entry_id", 0); beforeEntryID > 0 {
|
||||
builder.BeforeEntryID(beforeEntryID)
|
||||
}
|
||||
|
||||
afterEntryID := request.QueryInt64Param(r, "after_entry_id", 0)
|
||||
if afterEntryID > 0 {
|
||||
if afterEntryID := request.QueryInt64Param(r, "after_entry_id", 0); afterEntryID > 0 {
|
||||
builder.AfterEntryID(afterEntryID)
|
||||
}
|
||||
|
||||
beforeTimestamp := request.QueryInt64Param(r, "before", 0)
|
||||
if beforeTimestamp > 0 {
|
||||
builder.BeforeDate(time.Unix(beforeTimestamp, 0))
|
||||
if beforePublishedTimestamp := request.QueryInt64Param(r, "before", 0); beforePublishedTimestamp > 0 {
|
||||
builder.BeforePublishedDate(time.Unix(beforePublishedTimestamp, 0))
|
||||
}
|
||||
|
||||
afterTimestamp := request.QueryInt64Param(r, "after", 0)
|
||||
if afterTimestamp > 0 {
|
||||
builder.AfterDate(time.Unix(afterTimestamp, 0))
|
||||
if afterPublishedTimestamp := request.QueryInt64Param(r, "after", 0); afterPublishedTimestamp > 0 {
|
||||
builder.AfterPublishedDate(time.Unix(afterPublishedTimestamp, 0))
|
||||
}
|
||||
|
||||
categoryID := request.QueryInt64Param(r, "category_id", 0)
|
||||
if categoryID > 0 {
|
||||
if beforePublishedTimestamp := request.QueryInt64Param(r, "published_before", 0); beforePublishedTimestamp > 0 {
|
||||
builder.BeforePublishedDate(time.Unix(beforePublishedTimestamp, 0))
|
||||
}
|
||||
|
||||
if afterPublishedTimestamp := request.QueryInt64Param(r, "published_after", 0); afterPublishedTimestamp > 0 {
|
||||
builder.AfterPublishedDate(time.Unix(afterPublishedTimestamp, 0))
|
||||
}
|
||||
|
||||
if beforeChangedTimestamp := request.QueryInt64Param(r, "changed_before", 0); beforeChangedTimestamp > 0 {
|
||||
builder.BeforeChangedDate(time.Unix(beforeChangedTimestamp, 0))
|
||||
}
|
||||
|
||||
if afterChangedTimestamp := request.QueryInt64Param(r, "changed_after", 0); afterChangedTimestamp > 0 {
|
||||
builder.AfterChangedDate(time.Unix(afterChangedTimestamp, 0))
|
||||
}
|
||||
|
||||
if categoryID := request.QueryInt64Param(r, "category_id", 0); categoryID > 0 {
|
||||
builder.WithCategoryID(categoryID)
|
||||
}
|
||||
|
||||
@@ -315,8 +388,7 @@ func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
searchQuery := request.QueryStringParam(r, "search", "")
|
||||
if searchQuery != "" {
|
||||
if searchQuery := request.QueryStringParam(r, "search", ""); searchQuery != "" {
|
||||
builder.WithSearchQuery(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
+34
-11
@@ -5,9 +5,11 @@ package api // import "miniflux.app/v2/internal/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
@@ -24,14 +26,24 @@ func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make the feed category optional for clients who don't support categories.
|
||||
if feedCreationRequest.CategoryID == 0 {
|
||||
category, err := h.store.FirstCategory(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
feedCreationRequest.CategoryID = category.ID
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
feed, localizedError := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
|
||||
if localizedError != nil {
|
||||
json.ServerError(w, r, localizedError.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,9 +59,9 @@ func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err := feedHandler.RefreshFeed(h.store, userID, feedID, false)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
localizedError := feedHandler.RefreshFeed(h.store, userID, feedID, false)
|
||||
if localizedError != nil {
|
||||
json.ServerError(w, r, localizedError.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,15 +70,26 @@ func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
jobs, err := h.store.NewUserBatch(userID, h.store.CountFeeds(userID))
|
||||
|
||||
batchBuilder := h.store.NewBatchBuilder()
|
||||
batchBuilder.WithErrorLimit(config.Opts.PollingParsingErrorLimit())
|
||||
batchBuilder.WithoutDisabledFeeds()
|
||||
batchBuilder.WithNextCheckExpired()
|
||||
batchBuilder.WithUserID(userID)
|
||||
|
||||
jobs, err := batchBuilder.FetchJobs()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
h.pool.Push(jobs)
|
||||
}()
|
||||
slog.Info(
|
||||
"Triggered a manual refresh of all feeds from the API",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int("nb_jobs", len(jobs)),
|
||||
)
|
||||
|
||||
go h.pool.Push(jobs)
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
@@ -92,7 +115,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, originalFeed.ID, &feedModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
+22
-1
@@ -10,7 +10,7 @@ import (
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
)
|
||||
|
||||
func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
if !h.store.HasIcon(feedID) {
|
||||
@@ -35,3 +35,24 @@ func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) {
|
||||
Data: icon.DataURL(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) getIconByIconID(w http.ResponseWriter, r *http.Request) {
|
||||
iconID := request.RouteInt64Param(r, "iconID")
|
||||
|
||||
icon, err := h.store.IconByID(iconID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, &feedIconResponse{
|
||||
ID: icon.ID,
|
||||
MimeType: icon.MimeType,
|
||||
Data: icon.DataURL(),
|
||||
})
|
||||
}
|
||||
|
||||
+46
-11
@@ -5,11 +5,11 @@ package api // import "miniflux.app/v2/internal/api"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
)
|
||||
|
||||
@@ -40,25 +40,37 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
token := r.Header.Get("X-Auth-Token")
|
||||
|
||||
if token == "" {
|
||||
logger.Debug("[API][TokenAuth] [ClientIP=%s] No API Key provided, go to the next middleware", clientIP)
|
||||
slog.Debug("[API] Skipped API token authentication because no API Key has been provided",
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByAPIKey(token)
|
||||
if err != nil {
|
||||
logger.Error("[API][TokenAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API][TokenAuth] [ClientIP=%s] No user found with the given API key", clientIP)
|
||||
slog.Warn("[API] No user found with the provided API key",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API][TokenAuth] [ClientIP=%s] User authenticated: %s", clientIP, user.Username)
|
||||
slog.Info("[API] User authenticated successfully with the API Token Authentication",
|
||||
slog.Bool("authentication_successful", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", user.Username),
|
||||
)
|
||||
|
||||
m.store.SetLastLogin(user.ID)
|
||||
m.store.SetAPIKeyUsedTimestamp(user.ID, token)
|
||||
|
||||
@@ -84,37 +96,60 @@ func (m *middleware) basicAuth(next http.Handler) http.Handler {
|
||||
clientIP := request.ClientIP(r)
|
||||
username, password, authOK := r.BasicAuth()
|
||||
if !authOK {
|
||||
logger.Debug("[API][BasicAuth] [ClientIP=%s] No authentication headers sent", clientIP)
|
||||
slog.Warn("[API] No Basic HTTP Authentication header sent with the request",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Empty username or password", clientIP)
|
||||
slog.Warn("[API] Empty username or password provided during Basic HTTP Authentication",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.store.CheckPassword(username, password); err != nil {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
|
||||
slog.Warn("[API] Invalid username or password provided during Basic HTTP Authentication",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", username),
|
||||
)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
logger.Error("[API][BasicAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username)
|
||||
slog.Warn("[API] User not found while using Basic HTTP Authentication",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", username),
|
||||
)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API][BasicAuth] [ClientIP=%s] User authenticated: %s", clientIP, username)
|
||||
slog.Info("[API] User authenticated successfully with the Basic HTTP Authentication",
|
||||
slog.Bool("authentication_successful", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", username),
|
||||
)
|
||||
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
opmlHandler := opml.NewHandler(h.store)
|
||||
opml, err := opmlHandler.Export(request.UserID(r))
|
||||
opmlExport, err := opmlHandler.Export(request.UserID(r))
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
xml.OK(w, r, opml)
|
||||
xml.OK(w, r, opmlExport)
|
||||
}
|
||||
|
||||
func (h *handler) importFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -21,3 +21,13 @@ type entriesResponse struct {
|
||||
type feedCreationResponse struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
type versionResponse struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"build_date"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Compiler string `json:"compiler"`
|
||||
Arch string `json:"arch"`
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/reader/fetcher"
|
||||
"miniflux.app/v2/internal/reader/subscription"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
@@ -25,21 +28,33 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, finderErr := subscription.FindSubscriptions(
|
||||
var rssbridgeURL string
|
||||
intg, err := h.store.Integration(request.UserID(r))
|
||||
if err == nil && intg != nil && intg.RSSBridgeEnabled {
|
||||
rssbridgeURL = intg.RSSBridgeURL
|
||||
}
|
||||
|
||||
requestBuilder := fetcher.NewRequestBuilder()
|
||||
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
||||
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
||||
requestBuilder.WithUserAgent(subscriptionDiscoveryRequest.UserAgent, config.Opts.HTTPClientUserAgent())
|
||||
requestBuilder.WithCookie(subscriptionDiscoveryRequest.Cookie)
|
||||
requestBuilder.WithUsernameAndPassword(subscriptionDiscoveryRequest.Username, subscriptionDiscoveryRequest.Password)
|
||||
requestBuilder.UseProxy(subscriptionDiscoveryRequest.FetchViaProxy)
|
||||
requestBuilder.IgnoreTLSErrors(subscriptionDiscoveryRequest.AllowSelfSignedCertificates)
|
||||
requestBuilder.DisableHTTP2(subscriptionDiscoveryRequest.DisableHTTP2)
|
||||
|
||||
subscriptions, localizedError := subscription.NewSubscriptionFinder(requestBuilder).FindSubscriptions(
|
||||
subscriptionDiscoveryRequest.URL,
|
||||
subscriptionDiscoveryRequest.UserAgent,
|
||||
subscriptionDiscoveryRequest.Cookie,
|
||||
subscriptionDiscoveryRequest.Username,
|
||||
subscriptionDiscoveryRequest.Password,
|
||||
subscriptionDiscoveryRequest.FetchViaProxy,
|
||||
subscriptionDiscoveryRequest.AllowSelfSignedCertificates,
|
||||
rssbridgeURL,
|
||||
)
|
||||
if finderErr != nil {
|
||||
json.ServerError(w, r, finderErr)
|
||||
|
||||
if localizedError != nil {
|
||||
json.ServerError(w, r, localizedError.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if subscriptions == nil {
|
||||
if len(subscriptions) == 0 {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
|
||||
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
||||
json.BadRequest(w, r, errors.New("only administrators can change permissions of standard users"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (h *handler) userByID(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
user, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ func (h *handler) userByUsername(w http.ResponseWriter, r *http.Request) {
|
||||
username := request.RouteStringParam(r, "username")
|
||||
user, err := h.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if user.ID == request.UserID(r) {
|
||||
json.BadRequest(w, r, errors.New("You cannot remove yourself"))
|
||||
json.BadRequest(w, r, errors.New("you cannot remove yourself"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ func askCredentials() (string, string) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
if !term.IsTerminal(fd) {
|
||||
fmt.Fprintf(os.Stderr, "This is not a terminal, exiting.\n")
|
||||
os.Exit(1)
|
||||
printErrorAndExit(fmt.Errorf("this is not an interactive terminal, exiting"))
|
||||
}
|
||||
|
||||
fmt.Print("Enter Username: ")
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/metric"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
@@ -16,13 +16,18 @@ import (
|
||||
func runCleanupTasks(store *storage.Storage) {
|
||||
nbSessions := store.CleanOldSessions(config.Opts.CleanupRemoveSessionsDays())
|
||||
nbUserSessions := store.CleanOldUserSessions(config.Opts.CleanupRemoveSessionsDays())
|
||||
logger.Info("[Sessions] Removed %d application sessions and %d user sessions", nbSessions, nbUserSessions)
|
||||
slog.Info("Sessions cleanup completed",
|
||||
slog.Int64("application_sessions_removed", nbSessions),
|
||||
slog.Int64("user_sessions_removed", nbUserSessions),
|
||||
)
|
||||
|
||||
startTime := time.Now()
|
||||
if rowsAffected, err := store.ArchiveEntries(model.EntryStatusRead, config.Opts.CleanupArchiveReadDays(), config.Opts.CleanupArchiveBatchSize()); err != nil {
|
||||
logger.Error("[ArchiveReadEntries] %v", err)
|
||||
slog.Error("Unable to archive read entries", slog.Any("error", err))
|
||||
} else {
|
||||
logger.Info("[ArchiveReadEntries] %d entries changed", rowsAffected)
|
||||
slog.Info("Archiving read entries completed",
|
||||
slog.Int64("read_entries_archived", rowsAffected),
|
||||
)
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
metric.ArchiveEntriesDuration.WithLabelValues(model.EntryStatusRead).Observe(time.Since(startTime).Seconds())
|
||||
@@ -31,9 +36,11 @@ func runCleanupTasks(store *storage.Storage) {
|
||||
|
||||
startTime = time.Now()
|
||||
if rowsAffected, err := store.ArchiveEntries(model.EntryStatusUnread, config.Opts.CleanupArchiveUnreadDays(), config.Opts.CleanupArchiveBatchSize()); err != nil {
|
||||
logger.Error("[ArchiveUnreadEntries] %v", err)
|
||||
slog.Error("Unable to archive unread entries", slog.Any("error", err))
|
||||
} else {
|
||||
logger.Info("[ArchiveUnreadEntries] %d entries changed", rowsAffected)
|
||||
slog.Info("Archiving unread entries completed",
|
||||
slog.Int64("unread_entries_archived", rowsAffected),
|
||||
)
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
metric.ArchiveEntriesDuration.WithLabelValues(model.EntryStatusUnread).Observe(time.Since(startTime).Seconds())
|
||||
|
||||
+50
-23
@@ -6,11 +6,13 @@ package cli // import "miniflux.app/v2/internal/cli"
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/database"
|
||||
"miniflux.app/v2/internal/locale"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/ui/static"
|
||||
"miniflux.app/v2/internal/version"
|
||||
@@ -21,7 +23,7 @@ const (
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
flagCreateAdminHelp = "Create admin user"
|
||||
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
|
||||
flagResetPasswordHelp = "Reset user password"
|
||||
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
||||
flagDebugModeHelp = "Show debug logs"
|
||||
@@ -30,6 +32,7 @@ const (
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
|
||||
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
|
||||
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
|
||||
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
|
||||
)
|
||||
|
||||
// Parse parses command line arguments.
|
||||
@@ -49,6 +52,7 @@ func Parse() {
|
||||
flagHealthCheck string
|
||||
flagRefreshFeeds bool
|
||||
flagRunCleanupTasks bool
|
||||
flagExportUserFeeds string
|
||||
)
|
||||
|
||||
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
|
||||
@@ -67,6 +71,7 @@ func Parse() {
|
||||
flag.StringVar(&flagHealthCheck, "healthcheck", "", flagHealthCheckHelp)
|
||||
flag.BoolVar(&flagRefreshFeeds, "refresh-feeds", false, flagRefreshFeedsHelp)
|
||||
flag.BoolVar(&flagRunCleanupTasks, "run-cleanup-tasks", false, flagRunCleanupTasksHelp)
|
||||
flag.StringVar(&flagExportUserFeeds, "export-user-feeds", "", flagExportUserFeedsHelp)
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewParser()
|
||||
@@ -74,13 +79,13 @@ func Parse() {
|
||||
if flagConfigFile != "" {
|
||||
config.Opts, err = cfg.ParseFile(flagConfigFile)
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
config.Opts, err = cfg.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if flagConfigDump {
|
||||
@@ -88,12 +93,27 @@ func Parse() {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Opts.LogDateTime() {
|
||||
logger.EnableDateTime()
|
||||
if flagDebugMode {
|
||||
config.Opts.SetLogLevel("debug")
|
||||
}
|
||||
|
||||
if flagDebugMode || config.Opts.HasDebugMode() {
|
||||
logger.EnableDebug()
|
||||
logFile := config.Opts.LogFile()
|
||||
var logFileHandler io.Writer
|
||||
switch logFile {
|
||||
case "stdout":
|
||||
logFileHandler = os.Stdout
|
||||
case "stderr":
|
||||
logFileHandler = os.Stderr
|
||||
default:
|
||||
logFileHandler, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
printErrorAndExit(fmt.Errorf("unable to open log file: %v", err))
|
||||
}
|
||||
defer logFileHandler.(*os.File).Close()
|
||||
}
|
||||
|
||||
if err := InitializeDefaultLogger(config.Opts.LogLevel(), logFileHandler, config.Opts.LogFormat(), config.Opts.LogDateTime()); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if flagHealthCheck != "" {
|
||||
@@ -112,25 +132,23 @@ func Parse() {
|
||||
}
|
||||
|
||||
if config.Opts.IsDefaultDatabaseURL() {
|
||||
logger.Info("The default value for DATABASE_URL is used")
|
||||
slog.Info("The default value for DATABASE_URL is used")
|
||||
}
|
||||
|
||||
logger.Debug("Loading translations...")
|
||||
if err := locale.LoadCatalogMessages(); err != nil {
|
||||
logger.Fatal("Unable to load translations: %v", err)
|
||||
printErrorAndExit(fmt.Errorf("unable to load translations: %v", err))
|
||||
}
|
||||
|
||||
logger.Debug("Loading static assets...")
|
||||
if err := static.CalculateBinaryFileChecksums(); err != nil {
|
||||
logger.Fatal("Unable to calculate binary files checksum: %v", err)
|
||||
printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err))
|
||||
}
|
||||
|
||||
if err := static.GenerateStylesheetsBundles(); err != nil {
|
||||
logger.Fatal("Unable to generate Stylesheet bundles: %v", err)
|
||||
printErrorAndExit(fmt.Errorf("unable to generate stylesheets bundles: %v", err))
|
||||
}
|
||||
|
||||
if err := static.GenerateJavascriptBundles(); err != nil {
|
||||
logger.Fatal("Unable to generate Javascript bundles: %v", err)
|
||||
printErrorAndExit(fmt.Errorf("unable to generate javascript bundles: %v", err))
|
||||
}
|
||||
|
||||
db, err := database.NewConnectionPool(
|
||||
@@ -140,19 +158,19 @@ func Parse() {
|
||||
config.Opts.DatabaseConnectionLifetime(),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to initialize database connection pool: %v", err)
|
||||
printErrorAndExit(fmt.Errorf("unable to connect to database: %v", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
store := storage.NewStorage(db)
|
||||
|
||||
if err := store.Ping(); err != nil {
|
||||
logger.Fatal("Unable to connect to the database: %v", err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if flagMigrate {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
logger.Fatal(`%v`, err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -162,13 +180,18 @@ func Parse() {
|
||||
return
|
||||
}
|
||||
|
||||
if flagExportUserFeeds != "" {
|
||||
exportUserFeeds(store, flagExportUserFeeds)
|
||||
return
|
||||
}
|
||||
|
||||
if flagFlushSessions {
|
||||
flushSessions(store)
|
||||
return
|
||||
}
|
||||
|
||||
if flagCreateAdmin {
|
||||
createAdmin(store)
|
||||
createAdminUserFromInteractiveTerminal(store)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,17 +203,16 @@ func Parse() {
|
||||
// Run migrations and start the daemon.
|
||||
if config.Opts.RunMigrations() {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
logger.Fatal(`%v`, err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.IsSchemaUpToDate(db); err != nil {
|
||||
logger.Fatal(`You must run the SQL migrations, %v`, err)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
// Create admin user and start the daemon.
|
||||
if config.Opts.CreateAdmin() {
|
||||
createAdmin(store)
|
||||
createAdminUserFromEnvironmentVariables(store)
|
||||
}
|
||||
|
||||
if flagRefreshFeeds {
|
||||
@@ -205,3 +227,8 @@ func Parse() {
|
||||
|
||||
startDaemon(store)
|
||||
}
|
||||
|
||||
func printErrorAndExit(err error) {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -4,39 +4,47 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"log/slog"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
|
||||
func createAdmin(store *storage.Storage) {
|
||||
func createAdminUserFromEnvironmentVariables(store *storage.Storage) {
|
||||
createAdminUser(store, config.Opts.AdminUsername(), config.Opts.AdminPassword())
|
||||
}
|
||||
|
||||
func createAdminUserFromInteractiveTerminal(store *storage.Storage) {
|
||||
username, password := askCredentials()
|
||||
createAdminUser(store, username, password)
|
||||
}
|
||||
|
||||
func createAdminUser(store *storage.Storage, username, password string) {
|
||||
userCreationRequest := &model.UserCreationRequest{
|
||||
Username: config.Opts.AdminUsername(),
|
||||
Password: config.Opts.AdminPassword(),
|
||||
Username: username,
|
||||
Password: password,
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||
}
|
||||
|
||||
if store.UserExists(userCreationRequest.Username) {
|
||||
logger.Info(`User %q already exists, skipping creation`, userCreationRequest.Username)
|
||||
slog.Info("Skipping admin user creation because it already exists",
|
||||
slog.String("username", userCreationRequest.Username),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
printErrorAndExit(validationErr.Error())
|
||||
}
|
||||
|
||||
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
if user, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
printErrorAndExit(err)
|
||||
} else {
|
||||
slog.Info("Created new admin user",
|
||||
slog.String("username", user.Username),
|
||||
slog.Int64("user_id", user.ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-11
@@ -5,6 +5,7 @@ package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
httpd "miniflux.app/v2/internal/http/server"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/metric"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/systemd"
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func startDaemon(store *storage.Storage) {
|
||||
logger.Info("Starting daemon...")
|
||||
slog.Debug("Starting daemon...")
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
@@ -44,26 +44,25 @@ func startDaemon(store *storage.Storage) {
|
||||
}
|
||||
|
||||
if systemd.HasNotifySocket() {
|
||||
logger.Info("Sending readiness notification to Systemd")
|
||||
slog.Debug("Sending readiness notification to Systemd")
|
||||
|
||||
if err := systemd.SdNotify(systemd.SdNotifyReady); err != nil {
|
||||
logger.Error("Unable to send readiness notification to systemd: %v", err)
|
||||
slog.Error("Unable to send readiness notification to systemd", slog.Any("error", err))
|
||||
}
|
||||
|
||||
if config.Opts.HasWatchdog() && systemd.HasSystemdWatchdog() {
|
||||
logger.Info("Activating Systemd watchdog")
|
||||
slog.Debug("Activating Systemd watchdog")
|
||||
|
||||
go func() {
|
||||
interval, err := systemd.WatchdogInterval()
|
||||
if err != nil {
|
||||
logger.Error("Unable to parse watchdog interval from systemd: %v", err)
|
||||
slog.Error("Unable to get watchdog interval from systemd", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
err := store.Ping()
|
||||
if err != nil {
|
||||
logger.Error(`Systemd Watchdog: %v`, err)
|
||||
if err := store.Ping(); err != nil {
|
||||
slog.Error("Unable to ping database", slog.Any("error", err))
|
||||
} else {
|
||||
systemd.SdNotify(systemd.SdNotifyWatchdog)
|
||||
}
|
||||
@@ -75,7 +74,7 @@ func startDaemon(store *storage.Storage) {
|
||||
}
|
||||
|
||||
<-stop
|
||||
logger.Info("Shutting down the process...")
|
||||
slog.Debug("Shutting down the process")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -83,5 +82,5 @@ func startDaemon(store *storage.Storage) {
|
||||
httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
logger.Info("Process gracefully stopped")
|
||||
slog.Debug("Process gracefully stopped")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miniflux.app/v2/internal/reader/opml"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
)
|
||||
|
||||
func exportUserFeeds(store *storage.Storage, username string) {
|
||||
user, err := store.UserByUsername(username)
|
||||
if err != nil {
|
||||
printErrorAndExit(fmt.Errorf("unable to find user: %w", err))
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
printErrorAndExit(fmt.Errorf("user %q not found", username))
|
||||
}
|
||||
|
||||
opmlHandler := opml.NewHandler(store)
|
||||
opmlExport, err := opmlHandler.Export(user.ID)
|
||||
if err != nil {
|
||||
printErrorAndExit(fmt.Errorf("unable to export feeds: %w", err))
|
||||
}
|
||||
|
||||
fmt.Println(opmlExport)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"miniflux.app/v2/internal/storage"
|
||||
)
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
func flushSessions(store *storage.Storage) {
|
||||
fmt.Println("Flushing all sessions (disconnect users)")
|
||||
if err := store.FlushAllSessions(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
)
|
||||
|
||||
func doHealthCheck(healthCheckEndpoint string) {
|
||||
@@ -16,18 +17,18 @@ func doHealthCheck(healthCheckEndpoint string) {
|
||||
healthCheckEndpoint = "http://" + config.Opts.ListenAddr() + config.Opts.BasePath() + "/healthcheck"
|
||||
}
|
||||
|
||||
logger.Debug(`Executing health check on %s`, healthCheckEndpoint)
|
||||
slog.Debug("Executing health check request", slog.String("endpoint", healthCheckEndpoint))
|
||||
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := client.Get(healthCheckEndpoint)
|
||||
if err != nil {
|
||||
logger.Fatal(`Health check failure: %v`, err)
|
||||
printErrorAndExit(fmt.Errorf(`health check failure: %v`, err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Fatal(`Health check failed with status code %d`, resp.StatusCode)
|
||||
printErrorAndExit(fmt.Errorf(`health check failed with status code %d`, resp.StatusCode))
|
||||
}
|
||||
|
||||
logger.Debug(`Health check is OK`)
|
||||
slog.Debug(`Health check is passing`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func InitializeDefaultLogger(logLevel string, logFile io.Writer, logFormat string, logTime bool) error {
|
||||
var programLogLevel = new(slog.LevelVar)
|
||||
switch logLevel {
|
||||
case "debug":
|
||||
programLogLevel.Set(slog.LevelDebug)
|
||||
case "info":
|
||||
programLogLevel.Set(slog.LevelInfo)
|
||||
case "warning":
|
||||
programLogLevel.Set(slog.LevelWarn)
|
||||
case "error":
|
||||
programLogLevel.Set(slog.LevelError)
|
||||
}
|
||||
|
||||
logHandlerOptions := &slog.HandlerOptions{Level: programLogLevel}
|
||||
if !logTime {
|
||||
logHandlerOptions.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
var logger *slog.Logger
|
||||
switch logFormat {
|
||||
case "json":
|
||||
logger = slog.New(slog.NewJSONHandler(logFile, logHandlerOptions))
|
||||
default:
|
||||
logger = slog.New(slog.NewTextHandler(logFile, logHandlerOptions))
|
||||
}
|
||||
|
||||
slog.SetDefault(logger)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/model"
|
||||
feedHandler "miniflux.app/v2/internal/reader/handler"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
@@ -18,24 +18,50 @@ func refreshFeeds(store *storage.Storage) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
startTime := time.Now()
|
||||
jobs, err := store.NewBatch(config.Opts.BatchSize())
|
||||
|
||||
// Generate a batch of feeds for any user that has feeds to refresh.
|
||||
batchBuilder := store.NewBatchBuilder()
|
||||
batchBuilder.WithBatchSize(config.Opts.BatchSize())
|
||||
batchBuilder.WithErrorLimit(config.Opts.PollingParsingErrorLimit())
|
||||
batchBuilder.WithoutDisabledFeeds()
|
||||
batchBuilder.WithNextCheckExpired()
|
||||
|
||||
jobs, err := batchBuilder.FetchJobs()
|
||||
if err != nil {
|
||||
logger.Error("[Cronjob] %v", err)
|
||||
slog.Error("Unable to fetch jobs from database", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
nbJobs := len(jobs)
|
||||
logger.Info("[Cronjob]] Created %d jobs from a batch size of %d", nbJobs, config.Opts.BatchSize())
|
||||
|
||||
slog.Info("Created a batch of feeds",
|
||||
slog.Int("nb_jobs", nbJobs),
|
||||
slog.Int("batch_size", config.Opts.BatchSize()),
|
||||
)
|
||||
|
||||
var jobQueue = make(chan model.Job, nbJobs)
|
||||
|
||||
logger.Info("[Cronjob] Starting a pool of %d workers", config.Opts.WorkerPoolSize())
|
||||
for i := 0; i < config.Opts.WorkerPoolSize(); i++ {
|
||||
slog.Info("Starting a pool of workers",
|
||||
slog.Int("nb_workers", config.Opts.WorkerPoolSize()),
|
||||
)
|
||||
|
||||
for i := range config.Opts.WorkerPoolSize() {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for job := range jobQueue {
|
||||
logger.Info("[Cronjob] Refreshing feed #%d for user #%d in worker #%d", job.FeedID, job.UserID, workerID)
|
||||
if err := feedHandler.RefreshFeed(store, job.UserID, job.FeedID, false); err != nil {
|
||||
logger.Error("[Cronjob] Refreshing the feed #%d returned this error: %v", job.FeedID, err)
|
||||
slog.Info("Refreshing feed",
|
||||
slog.Int64("feed_id", job.FeedID),
|
||||
slog.Int64("user_id", job.UserID),
|
||||
slog.Int("worker_id", workerID),
|
||||
)
|
||||
|
||||
if localizedError := feedHandler.RefreshFeed(store, job.UserID, job.FeedID, false); localizedError != nil {
|
||||
slog.Warn("Unable to refresh feed",
|
||||
slog.Int64("feed_id", job.FeedID),
|
||||
slog.Int64("user_id", job.UserID),
|
||||
slog.Any("error", localizedError.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
@@ -47,5 +73,9 @@ func refreshFeeds(store *storage.Storage) {
|
||||
close(jobQueue)
|
||||
|
||||
wg.Wait()
|
||||
logger.Info("[Cronjob] Refreshed %d feed(s) in %s", nbJobs, time.Since(startTime))
|
||||
|
||||
slog.Info("Refreshed a batch of feeds",
|
||||
slog.Int("nb_feeds", nbJobs),
|
||||
slog.String("duration", time.Since(startTime).String()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
@@ -16,27 +15,23 @@ func resetPassword(store *storage.Storage) {
|
||||
username, password := askCredentials()
|
||||
user, err := store.UserByUsername(username)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
fmt.Fprintf(os.Stderr, "User not found!\n")
|
||||
os.Exit(1)
|
||||
printErrorAndExit(fmt.Errorf("user not found"))
|
||||
}
|
||||
|
||||
userModificationRequest := &model.UserModificationRequest{
|
||||
Password: &password,
|
||||
}
|
||||
if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
printErrorAndExit(validationErr.Error())
|
||||
}
|
||||
|
||||
user.Password = password
|
||||
if err := store.UpdateUser(user); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
fmt.Println("Password changed!")
|
||||
|
||||
@@ -4,22 +4,23 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/worker"
|
||||
)
|
||||
|
||||
func runScheduler(store *storage.Storage, pool *worker.Pool) {
|
||||
logger.Info(`Starting background scheduler...`)
|
||||
slog.Debug(`Starting background scheduler...`)
|
||||
|
||||
go feedScheduler(
|
||||
store,
|
||||
pool,
|
||||
config.Opts.PollingFrequency(),
|
||||
config.Opts.BatchSize(),
|
||||
config.Opts.PollingParsingErrorLimit(),
|
||||
)
|
||||
|
||||
go cleanupScheduler(
|
||||
@@ -28,13 +29,21 @@ func runScheduler(store *storage.Storage, pool *worker.Pool) {
|
||||
)
|
||||
}
|
||||
|
||||
func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSize int) {
|
||||
func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSize, errorLimit int) {
|
||||
for range time.Tick(time.Duration(frequency) * time.Minute) {
|
||||
jobs, err := store.NewBatch(batchSize)
|
||||
logger.Info("[Scheduler:Feed] Pushing %d jobs to the queue", len(jobs))
|
||||
if err != nil {
|
||||
logger.Error("[Scheduler:Feed] %v", err)
|
||||
} else {
|
||||
// Generate a batch of feeds for any user that has feeds to refresh.
|
||||
batchBuilder := store.NewBatchBuilder()
|
||||
batchBuilder.WithBatchSize(batchSize)
|
||||
batchBuilder.WithErrorLimit(errorLimit)
|
||||
batchBuilder.WithoutDisabledFeeds()
|
||||
batchBuilder.WithNextCheckExpired()
|
||||
|
||||
if jobs, err := batchBuilder.FetchJobs(); err != nil {
|
||||
slog.Error("Unable to fetch jobs from database", slog.Any("error", err))
|
||||
} else if len(jobs) > 0 {
|
||||
slog.Info("Created a batch of feeds",
|
||||
slog.Int("nb_jobs", len(jobs)),
|
||||
)
|
||||
pool.Push(jobs)
|
||||
}
|
||||
}
|
||||
|
||||
+443
-55
@@ -4,10 +4,186 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogFileDefaultValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogFile() != defaultLogFile {
|
||||
t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFileWithCustomFilename(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_FILE", "foobar.log")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
if opts.LogFile() != "foobar.log" {
|
||||
t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFileWithEmptyValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_FILE", "")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogFile() != defaultLogFile {
|
||||
t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLevelDefaultValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogLevel() != defaultLogLevel {
|
||||
t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLevelWithCustomValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_LEVEL", "warning")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogLevel() != "warning" {
|
||||
t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLevelWithInvalidValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_LEVEL", "invalid")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogLevel() != defaultLogLevel {
|
||||
t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogDateTimeDefaultValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogDateTime() != defaultLogDateTime {
|
||||
t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogDateTimeWithCustomValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_DATETIME", "false")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogDateTime() != false {
|
||||
t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogDateTimeWithInvalidValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_DATETIME", "invalid")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogDateTime() != defaultLogDateTime {
|
||||
t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFormatDefaultValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogFormat() != defaultLogFormat {
|
||||
t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFormatWithCustomValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_FORMAT", "json")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogFormat() != "json" {
|
||||
t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFormatWithInvalidValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("LOG_FORMAT", "invalid")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogFormat() != defaultLogFormat {
|
||||
t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugModeOn(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("DEBUG", "1")
|
||||
@@ -18,8 +194,8 @@ func TestDebugModeOn(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if !opts.HasDebugMode() {
|
||||
t.Fatalf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
|
||||
if opts.LogLevel() != "debug" {
|
||||
t.Fatalf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +208,8 @@ func TestDebugModeOff(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.HasDebugMode() {
|
||||
t.Fatalf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
|
||||
if opts.LogLevel() != "info" {
|
||||
t.Fatalf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,6 +760,41 @@ func TestPollingFrequency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautForceRefreshInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultForceRefreshInterval
|
||||
result := opts.ForceRefreshInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected FORCE_REFRESH_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceRefreshInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("FORCE_REFRESH_INTERVAL", "42")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 42
|
||||
result := opts.ForceRefreshInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected FORCE_REFRESH_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBatchSizeValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
@@ -654,7 +865,7 @@ func TestPollingScheduler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautSchedulerCountBasedMaxIntervalValue(t *testing.T) {
|
||||
func TestDefautSchedulerEntryFrequencyMaxIntervalValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
@@ -671,7 +882,7 @@ func TestDefautSchedulerCountBasedMaxIntervalValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautSchedulerCountBasedMaxInterval(t *testing.T) {
|
||||
func TestSchedulerEntryFrequencyMaxInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL", "30")
|
||||
|
||||
@@ -689,7 +900,7 @@ func TestDefautSchedulerCountBasedMaxInterval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautSchedulerCountBasedMinIntervalValue(t *testing.T) {
|
||||
func TestDefautSchedulerEntryFrequencyMinIntervalValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
@@ -706,7 +917,7 @@ func TestDefautSchedulerCountBasedMinIntervalValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautSchedulerCountBasedMinInterval(t *testing.T) {
|
||||
func TestSchedulerEntryFrequencyMinInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", "30")
|
||||
|
||||
@@ -741,7 +952,7 @@ func TestDefautSchedulerEntryFrequencyFactorValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautSchedulerEntryFrequencyFactor(t *testing.T) {
|
||||
func TestSchedulerEntryFrequencyFactor(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_FACTOR", "2")
|
||||
|
||||
@@ -759,6 +970,41 @@ func TestDefautSchedulerEntryFrequencyFactor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSchedulerRoundRobinValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultSchedulerRoundRobinMinInterval
|
||||
result := opts.SchedulerRoundRobinMinInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerRoundRobin(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SCHEDULER_ROUND_ROBIN_MIN_INTERVAL", "15")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 15
|
||||
result := opts.SchedulerRoundRobinMinInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollingParsingErrorLimit(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100")
|
||||
@@ -1197,9 +1443,9 @@ func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyOption(t *testing.T) {
|
||||
func TestMediaProxyMode(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1208,14 +1454,14 @@ func TestProxyOption(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := "all"
|
||||
result := opts.ProxyOption()
|
||||
result := opts.MediaProxyMode()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyOptionValue(t *testing.T) {
|
||||
func TestDefaultMediaProxyModeValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
@@ -1224,17 +1470,17 @@ func TestDefaultProxyOptionValue(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultProxyOption
|
||||
result := opts.ProxyOption()
|
||||
expected := defaultMediaProxyMode
|
||||
result := opts.MediaProxyMode()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypes(t *testing.T) {
|
||||
func TestMediaProxyResourceTypes(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1244,25 +1490,25 @@ func TestProxyMediaTypes(t *testing.T) {
|
||||
|
||||
expected := []string{"audio", "image"}
|
||||
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
|
||||
func TestMediaProxyResourceTypesWithDuplicatedValues(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio, image")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio, image")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1271,23 +1517,119 @@ func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := []string{"audio", "image"}
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
|
||||
func TestDefaultMediaProxyResourceTypes(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyHTTPClientTimeout(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 24
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultMediaProxyHTTPClientTimeoutValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultMediaProxyHTTPClientTimeout
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyCustomURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_CUSTOM_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyPrivateKey(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "foobar")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []byte("foobar")
|
||||
result := opts.MediaProxyPrivateKey()
|
||||
|
||||
if !bytes.Equal(result, expected) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImagesOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
@@ -1298,30 +1640,31 @@ func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
expectedProxyOption := "all"
|
||||
result := opts.ProxyOption()
|
||||
result := opts.MediaProxyMode()
|
||||
if result != expectedProxyOption {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyMediaTypes(t *testing.T) {
|
||||
func TestProxyImageURLForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGE_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1329,25 +1672,73 @@ func TestDefaultProxyMediaTypes(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_IMAGE_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
func TestProxyURLOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypesOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := []string{"audio", "image"}
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPClientTimeout(t *testing.T) {
|
||||
func TestProxyOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := "all"
|
||||
result := opts.MediaProxyMode()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPClientTimeoutOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
||||
|
||||
@@ -1356,29 +1747,26 @@ func TestProxyHTTPClientTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 24
|
||||
result := opts.ProxyHTTPClientTimeout()
|
||||
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyHTTPClientTimeoutValue(t *testing.T) {
|
||||
func TestProxyPrivateKeyOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "foobar")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultProxyHTTPClientTimeout
|
||||
result := opts.ProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
expected := []byte("foobar")
|
||||
result := opts.MediaProxyPrivateKey()
|
||||
if !bytes.Equal(result, expected) {
|
||||
t.Fatalf(`Unexpected PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1544,8 +1932,8 @@ Invalid text
|
||||
t.Errorf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.HasDebugMode() != true {
|
||||
t.Errorf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
|
||||
if opts.LogLevel() != "debug" {
|
||||
t.Errorf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
|
||||
expected := ">#1234"
|
||||
@@ -1699,7 +2087,7 @@ func TestParseConfigDumpOutput(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := tmpfile.Write([]byte(serialized)); err != nil {
|
||||
if _, err := tmpfile.WriteString(serialized); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
+102
-47
@@ -4,18 +4,21 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPS = false
|
||||
defaultLogFile = "stderr"
|
||||
defaultLogDateTime = false
|
||||
defaultLogFormat = "text"
|
||||
defaultLogLevel = "info"
|
||||
defaultHSTS = true
|
||||
defaultHTTPService = true
|
||||
defaultSchedulerService = true
|
||||
@@ -24,13 +27,15 @@ const (
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultRootURL = "http://localhost"
|
||||
defaultBasePath = ""
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultWorkerPoolSize = 16
|
||||
defaultPollingFrequency = 60
|
||||
defaultForceRefreshInterval = 30
|
||||
defaultBatchSize = 100
|
||||
defaultPollingScheduler = "round_robin"
|
||||
defaultSchedulerEntryFrequencyMinInterval = 5
|
||||
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
|
||||
defaultSchedulerEntryFrequencyFactor = 1
|
||||
defaultSchedulerRoundRobinMinInterval = 60
|
||||
defaultPollingParsingErrorLimit = 3
|
||||
defaultRunMigrations = false
|
||||
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
@@ -46,10 +51,11 @@ const (
|
||||
defaultCleanupArchiveUnreadDays = 180
|
||||
defaultCleanupArchiveBatchSize = 10000
|
||||
defaultCleanupRemoveSessionsDays = 30
|
||||
defaultProxyHTTPClientTimeout = 120
|
||||
defaultProxyOption = "http-only"
|
||||
defaultProxyMediaTypes = "image"
|
||||
defaultProxyUrl = ""
|
||||
defaultMediaProxyHTTPClientTimeout = 120
|
||||
defaultMediaProxyMode = "http-only"
|
||||
defaultMediaResourceTypes = "image"
|
||||
defaultMediaProxyURL = ""
|
||||
defaultFilterEntryMaxAgeDays = 0
|
||||
defaultFetchOdyseeWatchTime = false
|
||||
defaultFetchYouTubeWatchTime = false
|
||||
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
|
||||
@@ -78,6 +84,7 @@ const (
|
||||
defaultMetricsPassword = ""
|
||||
defaultWatchdog = true
|
||||
defaultInvidiousInstance = "yewtu.be"
|
||||
defaultWebAuthn = false
|
||||
)
|
||||
|
||||
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
@@ -91,11 +98,13 @@ type Option struct {
|
||||
// Options contains configuration options.
|
||||
type Options struct {
|
||||
HTTPS bool
|
||||
logFile string
|
||||
logDateTime bool
|
||||
logFormat string
|
||||
logLevel string
|
||||
hsts bool
|
||||
httpService bool
|
||||
schedulerService bool
|
||||
debug bool
|
||||
serverTimingHeader bool
|
||||
baseURL string
|
||||
rootURL string
|
||||
@@ -115,22 +124,25 @@ type Options struct {
|
||||
cleanupArchiveBatchSize int
|
||||
cleanupRemoveSessionsDays int
|
||||
pollingFrequency int
|
||||
forceRefreshInterval int
|
||||
batchSize int
|
||||
pollingScheduler string
|
||||
schedulerEntryFrequencyMinInterval int
|
||||
schedulerEntryFrequencyMaxInterval int
|
||||
schedulerEntryFrequencyFactor int
|
||||
schedulerRoundRobinMinInterval int
|
||||
pollingParsingErrorLimit int
|
||||
workerPoolSize int
|
||||
createAdmin bool
|
||||
adminUsername string
|
||||
adminPassword string
|
||||
proxyHTTPClientTimeout int
|
||||
proxyOption string
|
||||
proxyMediaTypes []string
|
||||
proxyUrl string
|
||||
mediaProxyHTTPClientTimeout int
|
||||
mediaProxyMode string
|
||||
mediaProxyResourceTypes []string
|
||||
mediaProxyCustomURL string
|
||||
fetchOdyseeWatchTime bool
|
||||
fetchYouTubeWatchTime bool
|
||||
filterEntryMaxAgeDays int
|
||||
youTubeEmbedUrlOverride string
|
||||
oauth2UserCreationAllowed bool
|
||||
oauth2ClientID string
|
||||
@@ -155,21 +167,21 @@ type Options struct {
|
||||
metricsPassword string
|
||||
watchdog bool
|
||||
invidiousInstance string
|
||||
proxyPrivateKey []byte
|
||||
mediaProxyPrivateKey []byte
|
||||
webAuthn bool
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
func NewOptions() *Options {
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
|
||||
return &Options{
|
||||
HTTPS: defaultHTTPS,
|
||||
logFile: defaultLogFile,
|
||||
logDateTime: defaultLogDateTime,
|
||||
logFormat: defaultLogFormat,
|
||||
logLevel: defaultLogLevel,
|
||||
hsts: defaultHSTS,
|
||||
httpService: defaultHTTPService,
|
||||
schedulerService: defaultSchedulerService,
|
||||
debug: defaultDebug,
|
||||
serverTimingHeader: defaultTiming,
|
||||
baseURL: defaultBaseURL,
|
||||
rootURL: defaultRootURL,
|
||||
@@ -189,18 +201,21 @@ func NewOptions() *Options {
|
||||
cleanupArchiveBatchSize: defaultCleanupArchiveBatchSize,
|
||||
cleanupRemoveSessionsDays: defaultCleanupRemoveSessionsDays,
|
||||
pollingFrequency: defaultPollingFrequency,
|
||||
forceRefreshInterval: defaultForceRefreshInterval,
|
||||
batchSize: defaultBatchSize,
|
||||
pollingScheduler: defaultPollingScheduler,
|
||||
schedulerEntryFrequencyMinInterval: defaultSchedulerEntryFrequencyMinInterval,
|
||||
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
|
||||
schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor,
|
||||
schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval,
|
||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||
workerPoolSize: defaultWorkerPoolSize,
|
||||
createAdmin: defaultCreateAdmin,
|
||||
proxyHTTPClientTimeout: defaultProxyHTTPClientTimeout,
|
||||
proxyOption: defaultProxyOption,
|
||||
proxyMediaTypes: []string{defaultProxyMediaTypes},
|
||||
proxyUrl: defaultProxyUrl,
|
||||
mediaProxyHTTPClientTimeout: defaultMediaProxyHTTPClientTimeout,
|
||||
mediaProxyMode: defaultMediaProxyMode,
|
||||
mediaProxyResourceTypes: []string{defaultMediaResourceTypes},
|
||||
mediaProxyCustomURL: defaultMediaProxyURL,
|
||||
filterEntryMaxAgeDays: defaultFilterEntryMaxAgeDays,
|
||||
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
|
||||
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
|
||||
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
|
||||
@@ -227,15 +242,35 @@ func NewOptions() *Options {
|
||||
metricsPassword: defaultMetricsPassword,
|
||||
watchdog: defaultWatchdog,
|
||||
invidiousInstance: defaultInvidiousInstance,
|
||||
proxyPrivateKey: randomKey,
|
||||
mediaProxyPrivateKey: crypto.GenerateRandomBytes(16),
|
||||
webAuthn: defaultWebAuthn,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) LogFile() string {
|
||||
return o.logFile
|
||||
}
|
||||
|
||||
// LogDateTime returns true if the date/time should be displayed in log messages.
|
||||
func (o *Options) LogDateTime() bool {
|
||||
return o.logDateTime
|
||||
}
|
||||
|
||||
// LogFormat returns the log format.
|
||||
func (o *Options) LogFormat() string {
|
||||
return o.logFormat
|
||||
}
|
||||
|
||||
// LogLevel returns the log level.
|
||||
func (o *Options) LogLevel() string {
|
||||
return o.logLevel
|
||||
}
|
||||
|
||||
// SetLogLevel sets the log level.
|
||||
func (o *Options) SetLogLevel(level string) {
|
||||
o.logLevel = level
|
||||
}
|
||||
|
||||
// HasMaintenanceMode returns true if maintenance mode is enabled.
|
||||
func (o *Options) HasMaintenanceMode() bool {
|
||||
return o.maintenanceMode
|
||||
@@ -246,11 +281,6 @@ func (o *Options) MaintenanceMessage() string {
|
||||
return o.maintenanceMessage
|
||||
}
|
||||
|
||||
// HasDebugMode returns true if debug mode is enabled.
|
||||
func (o *Options) HasDebugMode() bool {
|
||||
return o.debug
|
||||
}
|
||||
|
||||
// HasServerTimingHeader returns true if server-timing headers enabled.
|
||||
func (o *Options) HasServerTimingHeader() bool {
|
||||
return o.serverTimingHeader
|
||||
@@ -351,6 +381,11 @@ func (o *Options) PollingFrequency() int {
|
||||
return o.pollingFrequency
|
||||
}
|
||||
|
||||
// ForceRefreshInterval returns the force refresh interval
|
||||
func (o *Options) ForceRefreshInterval() int {
|
||||
return o.forceRefreshInterval
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (o *Options) BatchSize() int {
|
||||
return o.batchSize
|
||||
@@ -376,6 +411,10 @@ func (o *Options) SchedulerEntryFrequencyFactor() int {
|
||||
return o.schedulerEntryFrequencyFactor
|
||||
}
|
||||
|
||||
func (o *Options) SchedulerRoundRobinMinInterval() int {
|
||||
return o.schedulerRoundRobinMinInterval
|
||||
}
|
||||
|
||||
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
|
||||
func (o *Options) PollingParsingErrorLimit() int {
|
||||
return o.pollingParsingErrorLimit
|
||||
@@ -453,24 +492,29 @@ func (o *Options) FetchOdyseeWatchTime() bool {
|
||||
return o.fetchOdyseeWatchTime
|
||||
}
|
||||
|
||||
// ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) ProxyOption() string {
|
||||
return o.proxyOption
|
||||
// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) MediaProxyMode() string {
|
||||
return o.mediaProxyMode
|
||||
}
|
||||
|
||||
// ProxyMediaTypes returns a slice of media types to proxy.
|
||||
func (o *Options) ProxyMediaTypes() []string {
|
||||
return o.proxyMediaTypes
|
||||
// MediaProxyResourceTypes returns a slice of resource types to proxy.
|
||||
func (o *Options) MediaProxyResourceTypes() []string {
|
||||
return o.mediaProxyResourceTypes
|
||||
}
|
||||
|
||||
// ProxyUrl returns a string of a URL to use to proxy image requests
|
||||
func (o *Options) ProxyUrl() string {
|
||||
return o.proxyUrl
|
||||
// MediaCustomProxyURL returns the custom proxy URL for medias.
|
||||
func (o *Options) MediaCustomProxyURL() string {
|
||||
return o.mediaProxyCustomURL
|
||||
}
|
||||
|
||||
// ProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
||||
func (o *Options) ProxyHTTPClientTimeout() int {
|
||||
return o.proxyHTTPClientTimeout
|
||||
// MediaProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
||||
func (o *Options) MediaProxyHTTPClientTimeout() int {
|
||||
return o.mediaProxyHTTPClientTimeout
|
||||
}
|
||||
|
||||
// MediaProxyPrivateKey returns the private key used by the media proxy.
|
||||
func (o *Options) MediaProxyPrivateKey() []byte {
|
||||
return o.mediaProxyPrivateKey
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
@@ -566,9 +610,14 @@ func (o *Options) InvidiousInstance() string {
|
||||
return o.invidiousInstance
|
||||
}
|
||||
|
||||
// ProxyPrivateKey returns the private key used by the media proxy
|
||||
func (o *Options) ProxyPrivateKey() []byte {
|
||||
return o.proxyPrivateKey
|
||||
// WebAuthn returns true if WebAuthn logins are supported
|
||||
func (o *Options) WebAuthn() bool {
|
||||
return o.webAuthn
|
||||
}
|
||||
|
||||
// FilterEntryMaxAgeDays returns the number of days after which entries should be retained.
|
||||
func (o *Options) FilterEntryMaxAgeDays() int {
|
||||
return o.filterEntryMaxAgeDays
|
||||
}
|
||||
|
||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||
@@ -593,10 +642,10 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"DATABASE_MAX_CONNS": o.databaseMaxConns,
|
||||
"DATABASE_MIN_CONNS": o.databaseMinConns,
|
||||
"DATABASE_URL": redactSecretValue(o.databaseURL, redactSecret),
|
||||
"DEBUG": o.debug,
|
||||
"DISABLE_HSTS": !o.hsts,
|
||||
"DISABLE_HTTP_SERVICE": !o.httpService,
|
||||
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
|
||||
"FILTER_ENTRY_MAX_AGE_DAYS": o.filterEntryMaxAgeDays,
|
||||
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
|
||||
"FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime,
|
||||
"HTTPS": o.HTTPS,
|
||||
@@ -609,7 +658,10 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"INVIDIOUS_INSTANCE": o.invidiousInstance,
|
||||
"KEY_FILE": o.certKeyFile,
|
||||
"LISTEN_ADDR": o.listenAddr,
|
||||
"LOG_FILE": o.logFile,
|
||||
"LOG_DATE_TIME": o.logDateTime,
|
||||
"LOG_FORMAT": o.logFormat,
|
||||
"LOG_LEVEL": o.logLevel,
|
||||
"MAINTENANCE_MESSAGE": o.maintenanceMessage,
|
||||
"MAINTENANCE_MODE": o.maintenanceMode,
|
||||
"METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","),
|
||||
@@ -625,23 +677,26 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
||||
"POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret),
|
||||
"POLLING_FREQUENCY": o.pollingFrequency,
|
||||
"FORCE_REFRESH_INTERVAL": o.forceRefreshInterval,
|
||||
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
||||
"POLLING_SCHEDULER": o.pollingScheduler,
|
||||
"PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout,
|
||||
"PROXY_MEDIA_TYPES": o.proxyMediaTypes,
|
||||
"PROXY_OPTION": o.proxyOption,
|
||||
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
|
||||
"PROXY_URL": o.proxyUrl,
|
||||
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
|
||||
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
|
||||
"MEDIA_PROXY_MODE": o.mediaProxyMode,
|
||||
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
|
||||
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
|
||||
"ROOT_URL": o.rootURL,
|
||||
"RUN_MIGRATIONS": o.runMigrations,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor,
|
||||
"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval,
|
||||
"SCHEDULER_SERVICE": o.schedulerService,
|
||||
"SERVER_TIMING_HEADER": o.serverTimingHeader,
|
||||
"WATCHDOG": o.watchdog,
|
||||
"WORKER_POOL_SIZE": o.workerPoolSize,
|
||||
"YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride,
|
||||
"WEBAUTHN": o.webAuthn,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keyValues))
|
||||
|
||||
+62
-20
@@ -10,7 +10,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
url_parser "net/url"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -56,7 +57,7 @@ func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
@@ -72,10 +73,26 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
value := strings.TrimSpace(fields[1])
|
||||
|
||||
switch key {
|
||||
case "LOG_FILE":
|
||||
p.opts.logFile = parseString(value, defaultLogFile)
|
||||
case "LOG_DATE_TIME":
|
||||
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
|
||||
case "LOG_LEVEL":
|
||||
parsedValue := parseString(value, defaultLogLevel)
|
||||
if parsedValue == "debug" || parsedValue == "info" || parsedValue == "warning" || parsedValue == "error" {
|
||||
p.opts.logLevel = parsedValue
|
||||
}
|
||||
case "LOG_FORMAT":
|
||||
parsedValue := parseString(value, defaultLogFormat)
|
||||
if parsedValue == "json" || parsedValue == "text" {
|
||||
p.opts.logFormat = parsedValue
|
||||
}
|
||||
case "DEBUG":
|
||||
p.opts.debug = parseBool(value, defaultDebug)
|
||||
slog.Warn("The DEBUG environment variable is deprecated, use LOG_LEVEL instead")
|
||||
parsedValue := parseBool(value, defaultDebug)
|
||||
if parsedValue {
|
||||
p.opts.logLevel = "debug"
|
||||
}
|
||||
case "SERVER_TIMING_HEADER":
|
||||
p.opts.serverTimingHeader = parseBool(value, defaultTiming)
|
||||
case "BASE_URL":
|
||||
@@ -97,6 +114,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
||||
case "DATABASE_CONNECTION_LIFETIME":
|
||||
p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
|
||||
case "FILTER_ENTRY_MAX_AGE_DAYS":
|
||||
p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays)
|
||||
case "RUN_MIGRATIONS":
|
||||
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
||||
case "DISABLE_HSTS":
|
||||
@@ -127,6 +146,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
|
||||
case "POLLING_FREQUENCY":
|
||||
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
|
||||
case "FORCE_REFRESH_INTERVAL":
|
||||
p.opts.forceRefreshInterval = parseInt(value, defaultForceRefreshInterval)
|
||||
case "BATCH_SIZE":
|
||||
p.opts.batchSize = parseInt(value, defaultBatchSize)
|
||||
case "POLLING_SCHEDULER":
|
||||
@@ -137,22 +158,45 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
|
||||
case "SCHEDULER_ENTRY_FREQUENCY_FACTOR":
|
||||
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
|
||||
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
|
||||
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
|
||||
case "POLLING_PARSING_ERROR_LIMIT":
|
||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||
// kept for compatibility purpose
|
||||
case "PROXY_IMAGES":
|
||||
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||
slog.Warn("The PROXY_IMAGES environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "PROXY_HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.proxyHTTPClientTimeout = parseInt(value, defaultProxyHTTPClientTimeout)
|
||||
slog.Warn("The PROXY_HTTP_CLIENT_TIMEOUT environment variable is deprecated, use MEDIA_PROXY_HTTP_CLIENT_TIMEOUT instead")
|
||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
||||
case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
||||
case "PROXY_OPTION":
|
||||
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||
slog.Warn("The PROXY_OPTION environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "MEDIA_PROXY_MODE":
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "PROXY_MEDIA_TYPES":
|
||||
p.opts.proxyMediaTypes = parseStringList(value, []string{defaultProxyMediaTypes})
|
||||
// kept for compatibility purpose
|
||||
slog.Warn("The PROXY_MEDIA_TYPES environment variable is deprecated, use MEDIA_PROXY_RESOURCE_TYPES instead")
|
||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
||||
case "MEDIA_PROXY_RESOURCE_TYPES":
|
||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
||||
case "PROXY_IMAGE_URL":
|
||||
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||
slog.Warn("The PROXY_IMAGE_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "PROXY_URL":
|
||||
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||
slog.Warn("The PROXY_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "PROXY_PRIVATE_KEY":
|
||||
slog.Warn("The PROXY_PRIVATE_KEY environment variable is deprecated, use MEDIA_PROXY_PRIVATE_KEY instead")
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "MEDIA_PROXY_PRIVATE_KEY":
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "MEDIA_PROXY_CUSTOM_URL":
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "CREATE_ADMIN":
|
||||
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
||||
case "ADMIN_USERNAME":
|
||||
@@ -225,10 +269,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.watchdog = parseBool(value, defaultWatchdog)
|
||||
case "INVIDIOUS_INSTANCE":
|
||||
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
|
||||
case "PROXY_PRIVATE_KEY":
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.proxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "WEBAUTHN":
|
||||
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,19 +289,19 @@ func parseBaseURL(value string) (string, string, string, error) {
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
|
||||
url, err := url_parser.Parse(value)
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("config: invalid BASE_URL: %w", err)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(url.Scheme)
|
||||
scheme := strings.ToLower(parsedURL.Scheme)
|
||||
if scheme != "https" && scheme != "http" {
|
||||
return "", "", "", errors.New("config: invalid BASE_URL: scheme must be http or https")
|
||||
}
|
||||
|
||||
basePath := url.Path
|
||||
url.Path = ""
|
||||
return value, url.String(), basePath, nil
|
||||
basePath := parsedURL.Path
|
||||
parsedURL.Path = ""
|
||||
return value, parsedURL.String(), basePath, nil
|
||||
}
|
||||
|
||||
func parseBool(value string, fallback bool) bool {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -16,8 +17,7 @@ import (
|
||||
|
||||
// HashFromBytes returns a SHA-256 checksum of the input.
|
||||
func HashFromBytes(value []byte) string {
|
||||
sum := sha256.Sum256(value)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
return fmt.Sprintf("%x", sha256.Sum256(value))
|
||||
}
|
||||
|
||||
// Hash returns a SHA-256 checksum of a string.
|
||||
@@ -55,3 +55,12 @@ func GenerateSHA256Hmac(secret string, data []byte) string {
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func GenerateUUID() string {
|
||||
b := GenerateRandomBytes(16)
|
||||
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
func ConstantTimeCmp(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package database // import "miniflux.app/v2/internal/database"
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
// Postgresql driver import
|
||||
@@ -31,12 +32,13 @@ func Migrate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
fmt.Println("-> Current schema version:", currentVersion)
|
||||
fmt.Println("-> Latest schema version:", schemaVersion)
|
||||
slog.Debug("Running database migrations",
|
||||
slog.Int("current_version", currentVersion),
|
||||
slog.Int("latest_version", schemaVersion),
|
||||
)
|
||||
|
||||
for version := currentVersion; version < schemaVersion; version++ {
|
||||
newVersion := version + 1
|
||||
fmt.Println("* Migrating to version:", newVersion)
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
|
||||
@@ -776,4 +776,110 @@ var migrations = []func(tx *sql.Tx) error{
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN telegram_bot_disable_buttons bool default 'f';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
-- Speed up has_enclosure
|
||||
CREATE INDEX enclosures_entry_id_idx ON enclosures(entry_id);
|
||||
|
||||
-- Speed up unread page
|
||||
CREATE INDEX entries_user_status_published_idx ON entries(user_id, status, published_at);
|
||||
CREATE INDEX entries_user_status_created_idx ON entries(user_id, status, created_at);
|
||||
CREATE INDEX feeds_feed_id_hide_globally_idx ON feeds(id, hide_globally);
|
||||
|
||||
-- Speed up history page
|
||||
CREATE INDEX entries_user_status_changed_published_idx ON entries(user_id, status, changed_at, published_at);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN rssbridge_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN rssbridge_url text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE webauthn_credentials (
|
||||
handle bytea primary key,
|
||||
cred_id bytea unique not null,
|
||||
user_id int references users(id) on delete cascade not null,
|
||||
public_key bytea not null,
|
||||
attestation_type varchar(255) not null,
|
||||
aaguid bytea,
|
||||
sign_count bigint,
|
||||
clone_warning bool,
|
||||
name text,
|
||||
added_on timestamp with time zone default now(),
|
||||
last_seen_on timestamp with time zone default now()
|
||||
);
|
||||
`)
|
||||
return
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN omnivore_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN omnivore_api_key text default '';
|
||||
ALTER TABLE integrations ADD COLUMN omnivore_url text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN linkace_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN linkace_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN linkace_api_key text default '';
|
||||
ALTER TABLE integrations ADD COLUMN linkace_tags text default '';
|
||||
ALTER TABLE integrations ADD COLUMN linkace_is_private bool default 't';
|
||||
ALTER TABLE integrations ADD COLUMN linkace_check_disabled bool default 't';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN linkwarden_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN linkwarden_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN linkwarden_api_key text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN readeck_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_only_url bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_api_key text default '';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_labels text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN disable_http2 bool default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN media_playback_rate numeric default 1;`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
// the WHERE part speed-up the request a lot
|
||||
sql := `UPDATE entries SET tags = array_remove(tags, '') WHERE '' = ANY(tags);`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package errors // import "miniflux.app/v2/internal/errors"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miniflux.app/v2/internal/locale"
|
||||
)
|
||||
|
||||
// LocalizedError represents an error than could be translated to another language.
|
||||
type LocalizedError struct {
|
||||
message string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
// Error returns untranslated error message.
|
||||
func (l LocalizedError) Error() string {
|
||||
return fmt.Sprintf(l.message, l.args...)
|
||||
}
|
||||
|
||||
// Localize returns the translated error message.
|
||||
func (l LocalizedError) Localize(printer *locale.Printer) string {
|
||||
return printer.Printf(l.message, l.args...)
|
||||
}
|
||||
|
||||
// NewLocalizedError returns a new LocalizedError.
|
||||
func NewLocalizedError(message string, args ...interface{}) *LocalizedError {
|
||||
return &LocalizedError{message: message, args: args}
|
||||
}
|
||||
+77
-22
@@ -4,6 +4,7 @@
|
||||
package fever // import "miniflux.app/v2/internal/fever"
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,9 +13,8 @@ import (
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxy"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -80,7 +80,9 @@ is_spark equal to 1.
|
||||
*/
|
||||
func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching groups for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching groups",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
categories, err := h.store.Categories(userID)
|
||||
if err != nil {
|
||||
@@ -130,7 +132,9 @@ For the “Sparks” super group the items should be limited to feeds with an is
|
||||
*/
|
||||
func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
||||
slog.Debug("[Fever] Fetching feeds",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
feeds, err := h.store.Feeds(userID)
|
||||
if err != nil {
|
||||
@@ -183,7 +187,9 @@ A PHP/HTML example:
|
||||
*/
|
||||
func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching favicons for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching favicons",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
icons, err := h.store.Icons(userID)
|
||||
if err != nil {
|
||||
@@ -247,16 +253,24 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
case request.HasQueryParam(r, "since_id"):
|
||||
sinceID := request.QueryInt64Param(r, "since_id", 0)
|
||||
if sinceID > 0 {
|
||||
logger.Debug("[Fever] Fetching items since #%d for user #%d", sinceID, userID)
|
||||
slog.Debug("[Fever] Fetching items since a given date",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("since_id", sinceID),
|
||||
)
|
||||
builder.AfterEntryID(sinceID)
|
||||
}
|
||||
case request.HasQueryParam(r, "max_id"):
|
||||
maxID := request.QueryInt64Param(r, "max_id", 0)
|
||||
if maxID == 0 {
|
||||
logger.Debug("[Fever] Fetching most recent items for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching most recent items",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
builder.WithSorting("id", "DESC")
|
||||
} else if maxID > 0 {
|
||||
logger.Debug("[Fever] Fetching items before #%d for user #%d", maxID, userID)
|
||||
slog.Debug("[Fever] Fetching items before a given item ID",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("max_id", maxID),
|
||||
)
|
||||
builder.BeforeEntryID(maxID)
|
||||
builder.WithSorting("id", "DESC")
|
||||
}
|
||||
@@ -274,7 +288,9 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
builder.WithEntryIDs(itemIDs)
|
||||
}
|
||||
default:
|
||||
logger.Debug("[Fever] Fetching oldest items for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching oldest items",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
}
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
@@ -308,7 +324,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
FeedID: entry.FeedID,
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
HTML: proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content),
|
||||
HTML: mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, r.Host, entry.Content),
|
||||
URL: entry.URL,
|
||||
IsSaved: isSaved,
|
||||
IsRead: isRead,
|
||||
@@ -330,7 +346,9 @@ A request with the unread_item_ids argument will return one additional member:
|
||||
*/
|
||||
func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching unread items for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching unread items",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
@@ -361,7 +379,9 @@ with the remote Fever installation.
|
||||
*/
|
||||
func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching saved items for user #%d", userID)
|
||||
slog.Debug("[Fever] Fetching saved items",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStarred(true)
|
||||
@@ -389,7 +409,9 @@ id=? where ? is replaced with the id of the item to modify
|
||||
*/
|
||||
func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Receiving mark=item call for user #%d", userID)
|
||||
slog.Debug("[Fever] Receiving mark=item call",
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
entryID := request.FormInt64Value(r, "id")
|
||||
if entryID <= 0 {
|
||||
@@ -407,20 +429,32 @@ func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
logger.Debug("[Fever] Marking entry #%d but not found, ignored", entryID)
|
||||
slog.Debug("[Fever] Entry not found",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("entry_id", entryID),
|
||||
)
|
||||
json.OK(w, r, newBaseResponse())
|
||||
return
|
||||
}
|
||||
|
||||
switch r.FormValue("as") {
|
||||
case "read":
|
||||
logger.Debug("[Fever] Mark entry #%d as read for user #%d", entryID, userID)
|
||||
slog.Debug("[Fever] Mark entry as read",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("entry_id", entryID),
|
||||
)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||
case "unread":
|
||||
logger.Debug("[Fever] Mark entry #%d as unread for user #%d", entryID, userID)
|
||||
slog.Debug("[Fever] Mark entry as unread",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("entry_id", entryID),
|
||||
)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||
case "saved":
|
||||
logger.Debug("[Fever] Mark entry #%d as saved for user #%d", entryID, userID)
|
||||
slog.Debug("[Fever] Mark entry as saved",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("entry_id", entryID),
|
||||
)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -436,7 +470,10 @@ func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
case "unsaved":
|
||||
logger.Debug("[Fever] Mark entry #%d as unsaved for user #%d", entryID, userID)
|
||||
slog.Debug("[Fever] Mark entry as unsaved",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("entry_id", entryID),
|
||||
)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -457,7 +494,11 @@ func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] Mark feed #%d as read for user #%d before %v", feedID, userID, before)
|
||||
slog.Debug("[Fever] Mark feed as read before a given date",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("feed_id", feedID),
|
||||
slog.Time("before_ts", before),
|
||||
)
|
||||
|
||||
if feedID <= 0 {
|
||||
return
|
||||
@@ -465,7 +506,12 @@ func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
go func() {
|
||||
if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
|
||||
logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
|
||||
slog.Error("[Fever] Unable to mark feed as read",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("feed_id", feedID),
|
||||
slog.Time("before_ts", before),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -483,7 +529,11 @@ func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
||||
groupID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] Mark group #%d as read for user #%d before %v", groupID, userID, before)
|
||||
slog.Debug("[Fever] Mark group as read before a given date",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("group_id", groupID),
|
||||
slog.Time("before_ts", before),
|
||||
)
|
||||
|
||||
if groupID < 0 {
|
||||
return
|
||||
@@ -499,7 +549,12 @@ func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Fever] MarkCategoryAsRead failed: %v", err)
|
||||
slog.Error("[Fever] Unable to mark group as read",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Int64("group_id", groupID),
|
||||
slog.Time("before_ts", before),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ package fever // import "miniflux.app/v2/internal/fever"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
)
|
||||
|
||||
@@ -26,25 +26,45 @@ func (m *middleware) serve(next http.Handler) http.Handler {
|
||||
clientIP := request.ClientIP(r)
|
||||
apiKey := r.FormValue("api_key")
|
||||
if apiKey == "" {
|
||||
logger.Info("[Fever] [ClientIP=%s] No API key provided", clientIP)
|
||||
slog.Warn("[Fever] No API key provided",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByFeverToken(apiKey)
|
||||
if err != nil {
|
||||
logger.Error("[Fever] %v", err)
|
||||
slog.Error("[Fever] Unable to fetch user by API key",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Info("[Fever] [ClientIP=%s] No user found with this API key", clientIP)
|
||||
slog.Warn("[Fever] No user found with the API key provided",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Fever] [ClientIP=%s] User #%d is authenticated with user agent %q", clientIP, user.ID, r.UserAgent())
|
||||
slog.Info("[Fever] User authenticated successfully",
|
||||
slog.Bool("authentication_successful", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Int64("user_id", user.ID),
|
||||
slog.String("username", user.Username),
|
||||
)
|
||||
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
+416
-213
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user