Compare commits

..

No commits in common. "main" and "2.0.1" have entirely different histories.
main ... 2.0.1

3141 changed files with 1264207 additions and 84059 deletions

View File

@ -1,31 +0,0 @@
{
"name": "Miniflux",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"remoteUser": "vscode",
"forwardPorts": [
8080
],
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"customizations": {
"vscode": {
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
"extensions": [
"ms-azuretools.vscode-docker",
"golang.go",
"rangav.vscode-thunder-client",
"GitHub.codespaces",
"GitHub.copilot",
"GitHub.copilot-chat"
]
}
}
}

View File

@ -1,31 +0,0 @@
version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/go:1.22
volumes:
- ..:/workspace:cached
command: sleep infinity
network_mode: service:db
environment:
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
db:
image: postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
hostname: postgres
environment:
POSTGRES_DB: miniflux2
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
apprise:
image: caronc/apprise:1.0
restart: unless-stopped
hostname: apprise
volumes:
postgres-data: null

View File

@ -1,10 +0,0 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug, triage needed
assignees: ''
---

View File

@ -1 +0,0 @@
blank_issues_enabled: false

View File

@ -1,10 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
- [ ] I have read this document: https://miniflux.app/opinionated.html#feature-request

View File

@ -1,10 +0,0 @@
---
name: Feed Problems
about: Problems with a feed or a website
title: ''
labels: feed problems, triage needed
assignees: ''
---

View File

@ -1,55 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "docker"
directory: "/packaging/docker/alpine"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "docker"
directory: "/packaging/docker/distroless"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "docker"
directory: "packaging/debian"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "docker"
directory: "packaging/rpm"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"

View File

@ -1,7 +0,0 @@
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

View File

@ -1,29 +0,0 @@
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

View File

@ -1,43 +0,0 @@
name: "CodeQL"
permissions: read-all
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '45 22 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22.x"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@ -1,82 +0,0 @@
name: Debian Packages
permissions: read-all
on:
workflow_dispatch:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches: [ main ]
jobs:
test-packages:
if: github.event.pull_request
name: Test Packages
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: 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_name == 'push'
name: Publish Packages
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: List generated files
run: ls -l *.deb
- name: Upload packages to repository
env:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
run: for f in *.deb; do curl -F package=@$f https://$FURY_TOKEN@push.fury.io/miniflux/; done

View File

@ -1,95 +0,0 @@
name: Docker
on:
schedule:
- cron: '0 1 * * *'
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches: [ main ]
jobs:
docker-images:
name: Docker Images
permissions:
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate Alpine Docker tags
id: docker_alpine_tags
uses: docker/metadata-action@v5
with:
images: |
docker.io/${{ github.repository_owner }}/miniflux
ghcr.io/${{ github.repository_owner }}/miniflux
quay.io/${{ github.repository_owner }}/miniflux
tags: |
type=ref,event=pr
type=schedule,pattern=nightly
type=semver,pattern={{raw}}
- name: Generate Distroless Docker tags
id: docker_distroless_tags
uses: docker/metadata-action@v5
with:
images: |
docker.io/${{ github.repository_owner }}/miniflux
ghcr.io/${{ github.repository_owner }}/miniflux
quay.io/${{ github.repository_owner }}/miniflux
tags: |
type=ref,event=pr
type=schedule,pattern=nightly
type=semver,pattern={{raw}}
flavor: |
suffix=-distroless,onlatest=true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Quay Container Registry
if: github.event_name != 'pull_request'
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@v6
with:
context: .
file: ./packaging/docker/alpine/Dockerfile
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_alpine_tags.outputs.tags }}
- name: Build and Push Distroless images
uses: docker/build-push-action@v6
with:
context: .
file: ./packaging/docker/distroless/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_distroless_tags.outputs.tags }}

View File

@ -1,39 +0,0 @@
name: Linters
permissions: read-all
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
jshint:
name: Javascript Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install linters
run: |
sudo npm install -g jshint@2.13.6 eslint@8.57.0
- name: Run jshint
run: jshint internal/ui/static/js/*.js
- name: Run ESLint
run: eslint internal/ui/static/js/*.js
golangci:
name: Golang Linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22.x"
- run: "go vet ./..."
- uses: golangci/golangci-lint-action@v6
with:
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

View File

@ -1,55 +0,0 @@
name: RPM Packages
permissions: read-all
on:
workflow_dispatch:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches: [ main ]
jobs:
test-package:
if: github.event.pull_request
name: Test Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build RPM Package
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_name == 'push'
name: Publish Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build RPM Package
run: make rpm
- name: List generated files
run: ls -l *.rpm
- name: Upload package to repository
env:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
run: for f in *.rpm; do curl -F package=@$f https://$FURY_TOKEN@push.fury.io/miniflux/; done

View File

@ -1,56 +0,0 @@
name: Tests
permissions: read-all
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
unit-tests:
name: Unit Tests
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
go-version: ["1.22.x"]
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
- name: Run unit tests
run: make test
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:9.5
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22.x"
- name: Checkout
uses: actions/checkout@v4
- name: Install Postgres client
run: sudo apt update && sudo apt install -y postgresql-client
- name: Run integration tests
run: make integration-test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PGHOST: 127.0.0.1
PGPASSWORD: postgres

10
.gitignore vendored
View File

@ -1,6 +1,4 @@
miniflux-*
./miniflux
*.rpm
*.deb
.idea
.vscode
miniflux-linux-amd64
miniflux-linux-arm*
miniflux-darwin-amd64
miniflux-test

17
.travis.yml Normal file
View File

@ -0,0 +1,17 @@
notifications:
email: false
services:
- postgresql
addons:
postgresql: "9.4"
language: go
go:
- 1.9
before_install:
- npm install -g jshint
- go get -u github.com/golang/lint/golint
script:
- jshint ui/static/js/app.js
- make lint
- make test
- make integration-test

1541
ChangeLog

File diff suppressed because it is too large Load Diff

105
Gopkg.lock generated Normal file
View File

@ -0,0 +1,105 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/PuerkitoBio/goquery"
packages = ["."]
revision = "e1271ee34c6a305e38566ecd27ae374944907ee9"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/andybalholm/cascadia"
packages = ["."]
revision = "349dd0209470eabd9514242c688c403c0926d266"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
version = "v1.6.0"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [".","hstore","oid"]
revision = "83612a56d3dd153a94a629cd64925371c9adad78"
[[projects]]
branch = "master"
name = "github.com/miniflux/miniflux-go"
packages = ["."]
revision = "887ba3b062946784f0e64edb1734f435beb204f9"
[[projects]]
name = "github.com/tdewolff/minify"
packages = [".","css","js"]
revision = "222672169d634c440a73abc47685074e1a9daa60"
version = "v2.3.4"
[[projects]]
name = "github.com/tdewolff/parse"
packages = [".","buffer","css","js","strconv"]
revision = "639f6272aec6b52094db77b9ec488214b0b4b1a1"
version = "v2.3.2"
[[projects]]
branch = "master"
name = "github.com/tomasen/realip"
packages = ["."]
revision = "b5850897b7b539a1c9f22cdaa3b547d1bd453db8"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["acme","acme/autocert","bcrypt","blowfish","ssh/terminal"]
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp","html","html/atom","html/charset"]
revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [".","internal"]
revision = "462316686f20eb6df426961c1c131bdaa5dfa68e"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "571f7bbbe08da2a8955aed9d4db316e78630e9a3"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["encoding","encoding/charmap","encoding/htmlindex","encoding/internal","encoding/internal/identifier","encoding/japanese","encoding/korean","encoding/simplifiedchinese","encoding/traditionalchinese","encoding/unicode","internal/gen","internal/tag","internal/utf8internal","language","runes","transform","unicode/cldr"]
revision = "d5a9226ed7dd70cade6ccae9d37517fe14dd9fee"
[[projects]]
name = "google.golang.org/appengine"
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "338222e5111416c46b2b8bde149443abc542b386dd02aff2a0dd6e13334bcf28"
solver-name = "gps-cdcl"
solver-version = 1

58
Gopkg.toml Normal file
View File

@ -0,0 +1,58 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/PuerkitoBio/goquery"
version = "1.1.0"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.0"
[[constraint]]
branch = "master"
name = "github.com/lib/pq"
[[constraint]]
branch = "master"
name = "github.com/miniflux/miniflux-go"
[[constraint]]
name = "github.com/tdewolff/minify"
version = "2.3.3"
[[constraint]]
branch = "master"
name = "github.com/tomasen/realip"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "master"
name = "golang.org/x/oauth2"

182
Makefile
View File

@ -1,177 +1,53 @@
APP := miniflux
DOCKER_IMAGE := miniflux/miniflux
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/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
DOCKER_PLATFORM := amd64
APP := miniflux
VERSION=$(shell git rev-parse --short HEAD)
BUILD_DATE=`date +%FT%T%z`
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
export PGPASSWORD := postgres
.PHONY: linux linux-arm darwin build run clean test lint integration-test clean-integration-test
.PHONY: \
miniflux \
miniflux-no-pie \
linux-amd64 \
linux-arm64 \
linux-armv7 \
linux-armv6 \
linux-armv5 \
linux-x86 \
darwin-amd64 \
darwin-arm64 \
freebsd-amd64 \
freebsd-x86 \
openbsd-amd64 \
openbsd-x86 \
netbsd-x86 \
netbsd-amd64 \
windows-amd64 \
windows-x86 \
build \
run \
clean \
test \
lint \
integration-test \
clean-integration-test \
docker-image \
docker-image-distroless \
docker-images \
rpm \
debian \
debian-packages
linux:
@ go generate
@ GOOS=linux GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-amd64 main.go
miniflux:
@ go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
linux-arm:
@ go generate
@ GOOS=linux GOARCH=arm64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv8 main.go
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv7 main.go
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv6 main.go
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv5 main.go
miniflux-no-pie:
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
darwin:
@ go generate
@ GOOS=darwin GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-darwin-amd64 main.go
linux-amd64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-arm64:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv7:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv6:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-armv5:
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
darwin-amd64:
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
darwin-arm64:
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
freebsd-amd64:
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
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)-$@.exe main.go
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
# NOTE: unsupported targets
netbsd-amd64:
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
linux-x86:
@ CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
freebsd-x86:
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
netbsd-x86:
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
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)-$@.exe main.go
build: linux linux-arm darwin
run:
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
@ go generate
@ go run main.go
clean:
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe
@ rm -f $(APP)-*
test:
go test -cover -race -count=1 ./...
go test -cover -race ./...
lint:
go vet ./...
staticcheck ./...
golangci-lint run --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
@ golint -set_exit_status ${PKG_LIST}
integration-test:
psql -U postgres -c 'drop database if exists miniflux_test;'
psql -U postgres -c 'create database miniflux_test;'
DATABASE_URL=$(DB_URL) go run main.go -migrate
DATABASE_URL=$(DB_URL) ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go -create-admin
go build -o miniflux-test main.go
DATABASE_URL=$(DB_URL) \
ADMIN_USERNAME=admin \
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
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
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
DATABASE_URL=$(DB_URL) ./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! echo exit | nc localhost 8080; do sleep 1; done >/dev/null
go test -v -tags=integration || cat /tmp/miniflux.log
clean-integration-test:
@ kill -9 `cat /tmp/miniflux.pid`
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
@ rm miniflux-test
@ psql -U postgres -c 'drop database if exists miniflux_test;'
docker-image:
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 .
docker-images:
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
--file packaging/docker/alpine/Dockerfile \
--tag $(DOCKER_IMAGE):$(VERSION) \
--push .
rpm: clean
@ docker build \
-t miniflux-rpm-builder \
-f packaging/rpm/Dockerfile \
.
@ docker run --rm \
-v ${PWD}:/root/rpmbuild/RPMS/x86_64 miniflux-rpm-builder \
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
debian:
@ docker buildx build --load \
--platform linux/$(DOCKER_PLATFORM) \
-t miniflux-deb-builder \
-f packaging/debian/Dockerfile \
.
@ docker run --rm --platform linux/$(DOCKER_PLATFORM) \
-v ${PWD}:/pkg miniflux-deb-builder
debian-packages: clean
$(MAKE) debian DOCKER_PLATFORM=amd64
$(MAKE) debian DOCKER_PLATFORM=arm64
$(MAKE) debian DOCKER_PLATFORM=arm/v7

View File

@ -1 +0,0 @@
web: miniflux.app

View File

@ -1,5 +1,8 @@
Miniflux 2
==========
[![Build Status](https://travis-ci.org/miniflux/miniflux.svg?branch=master)](https://travis-ci.org/miniflux/miniflux)
[![GoDoc](https://godoc.org/github.com/miniflux/miniflux?status.svg)](https://godoc.org/github.com/miniflux/miniflux)
[![Documentation Status](https://readthedocs.org/projects/miniflux/badge/?version=latest)](https://docs.miniflux.net/)
Miniflux is a minimalist and opinionated feed reader:
@ -8,47 +11,26 @@ Miniflux is a minimalist and opinionated feed reader:
- Doesn't use any ORM
- Doesn't use any complicated framework
- Use only modern vanilla Javascript (ES6 and Fetch API)
- Single binary compiled statically without dependency
- The number of features is voluntarily limited
It's simple, fast, lightweight and super easy to install.
Official website: <https://miniflux.app>
Miniflux 2 is a rewrite of [Miniflux 1.x](https://github.com/miniflux/miniflux-legacy) in Golang.
Documentation
-------------
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
The Miniflux documentation is available here: <https://docs.miniflux.net/>
- [Opinionated?](https://miniflux.app/opinionated.html)
- [Features](https://miniflux.app/features.html)
- [Requirements](https://miniflux.app/docs/requirements.html)
- [Installation Instructions](https://miniflux.app/docs/installation.html)
- [Upgrading to a New Version](https://miniflux.app/docs/upgrade.html)
- [Configuration](https://miniflux.app/docs/configuration.html)
- [Command Line Usage](https://miniflux.app/docs/cli.html)
- [User Interface Usage](https://miniflux.app/docs/ui.html)
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
- [Integration with External Services](https://miniflux.app/docs/services.html)
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
- [API Reference](https://miniflux.app/docs/api.html)
- [Development](https://miniflux.app/docs/development.html)
- [Internationalization](https://miniflux.app/docs/i18n.html)
- [Frequently Asked Questions](https://miniflux.app/faq.html)
Screenshots
-----------
Default theme:
![Default theme](https://miniflux.app/images/overview.png)
Dark theme when using keyboard navigation:
![Dark theme](https://miniflux.app/images/item-selection-black-theme.png)
- [Opinionated?](https://docs.miniflux.net/en/latest/opinionated.html)
- [Features](https://docs.miniflux.net/en/latest/features.html)
- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
- [Installation](https://docs.miniflux.net/en/latest/installation.html)
- [Upgrading to a new version](https://docs.miniflux.net/en/latest/upgrade.html)
- [Configuration](https://docs.miniflux.net/en/latest/configuration.html)
Credits
-------
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
- Author: Frédéric Guillot
- Distributed under Apache 2.0 License

View File

@ -1,11 +0,0 @@
# Security Policy
## Supported Versions
Only the latest stable version is supported.
## Reporting a Vulnerability
Preferably, [report the vulnerability privately using GitHub](https://github.com/miniflux/v2/security/advisories/new) ([documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)).
If you do not want to use GitHub, send an email to `security AT miniflux DOT net` with all the steps to reproduce the problem.

103
api/category.go Normal file
View File

@ -0,0 +1,103 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"github.com/miniflux/miniflux/http/handler"
)
// CreateCategory is the API handler to create a new category.
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
category, err := decodeCategoryPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
category.UserID = userID
if err := category.ValidateCategoryCreation(); err != nil {
response.JSON().BadRequest(err)
return
}
if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
response.JSON().BadRequest(errors.New("This category already exists"))
return
}
err = c.store.CreateCategory(category)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this category"))
return
}
response.JSON().Created(category)
}
// UpdateCategory is the API handler to update a category.
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
categoryID, err := request.IntegerParam("categoryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
category, err := decodeCategoryPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
category.UserID = ctx.UserID()
category.ID = categoryID
if err := category.ValidateCategoryModification(); err != nil {
response.JSON().BadRequest(err)
return
}
err = c.store.UpdateCategory(category)
if err != nil {
response.JSON().ServerError(errors.New("Unable to update this category"))
return
}
response.JSON().Created(category)
}
// GetCategories is the API handler to get a list of categories for a given user.
func (c *Controller) GetCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
categories, err := c.store.Categories(ctx.UserID())
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch categories"))
return
}
response.JSON().Standard(categories)
}
// RemoveCategory is the API handler to remove a category.
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
categoryID, err := request.IntegerParam("categoryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
if !c.store.CategoryExists(userID, categoryID) {
response.JSON().NotFound(errors.New("Category not found"))
return
}
if err := c.store.RemoveCategory(userID, categoryID); err != nil {
response.JSON().ServerError(errors.New("Unable to remove this category"))
return
}
response.JSON().NoContent()
}

21
api/controller.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/storage"
)
// Controller holds all handlers for the API.
type Controller struct {
store *storage.Storage
feedHandler *feed.Handler
}
// NewController creates a new controller.
func NewController(store *storage.Storage, feedHandler *feed.Handler) *Controller {
return &Controller{store: store, feedHandler: feedHandler}
}

10
api/doc.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package api implements API endpoints for Miniflux application.
*/
package api

223
api/entry.go Normal file
View File

@ -0,0 +1,223 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/model"
)
// GetFeedEntry is the API handler to get a single feed entry.
func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithFeedID(feedID)
builder.WithEntryID(entryID)
entry, err := builder.GetEntry()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
return
}
if entry == nil {
response.JSON().NotFound(errors.New("Entry not found"))
return
}
response.JSON().Standard(entry)
}
// GetEntry is the API handler to get a single entry.
func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithEntryID(entryID)
entry, err := builder.GetEntry()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
return
}
if entry == nil {
response.JSON().NotFound(errors.New("Entry not found"))
return
}
response.JSON().Standard(entry)
}
// GetFeedEntries is the API handler to get all feed entries.
func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
status := request.QueryStringParam("status", "")
if status != "" {
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
return
}
}
order := request.QueryStringParam("order", model.DefaultSortingOrder)
if err := model.ValidateEntryOrder(order); err != nil {
response.JSON().BadRequest(err)
return
}
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
if err := model.ValidateDirection(direction); err != nil {
response.JSON().BadRequest(err)
return
}
limit := request.QueryIntegerParam("limit", 100)
offset := request.QueryIntegerParam("offset", 0)
if err := model.ValidateRange(offset, limit); err != nil {
response.JSON().BadRequest(err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithFeedID(feedID)
builder.WithStatus(status)
builder.WithOrder(order)
builder.WithDirection(direction)
builder.WithOffset(offset)
builder.WithLimit(limit)
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
return
}
count, err := builder.CountEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
return
}
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
}
// GetEntries is the API handler to fetch entries.
func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
status := request.QueryStringParam("status", "")
if status != "" {
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
return
}
}
order := request.QueryStringParam("order", model.DefaultSortingOrder)
if err := model.ValidateEntryOrder(order); err != nil {
response.JSON().BadRequest(err)
return
}
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
if err := model.ValidateDirection(direction); err != nil {
response.JSON().BadRequest(err)
return
}
limit := request.QueryIntegerParam("limit", 100)
offset := request.QueryIntegerParam("offset", 0)
if err := model.ValidateRange(offset, limit); err != nil {
response.JSON().BadRequest(err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStatus(status)
builder.WithOrder(order)
builder.WithDirection(direction)
builder.WithOffset(offset)
builder.WithLimit(limit)
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
return
}
count, err := builder.CountEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
return
}
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
}
// SetEntryStatus is the API handler to change the status of entries.
func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
if err != nil {
response.JSON().BadRequest(errors.New("Invalid JSON payload"))
return
}
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
return
}
if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
response.JSON().ServerError(errors.New("Unable to change entries status"))
return
}
response.JSON().NoContent()
}
// ToggleBookmark is the API handler to toggle bookmark status.
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
return
}
response.JSON().NoContent()
}

192
api/feed.go Normal file
View File

@ -0,0 +1,192 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"github.com/miniflux/miniflux/reader/opml"
"github.com/miniflux/miniflux/http/handler"
)
// CreateFeed is the API handler to create a new feed.
func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedURL, categoryID, err := decodeFeedCreationPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
if feedURL == "" {
response.JSON().BadRequest(errors.New("The feed_url is required"))
return
}
if categoryID <= 0 {
response.JSON().BadRequest(errors.New("The category_id is required"))
return
}
if c.store.FeedURLExists(userID, feedURL) {
response.JSON().BadRequest(errors.New("This feed_url already exists"))
return
}
if !c.store.CategoryExists(userID, categoryID) {
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return
}
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this feed"))
return
}
type result struct {
FeedID int64 `json:"feed_id"`
}
response.JSON().Created(&result{FeedID: feed.ID})
}
// RefreshFeed is the API handler to refresh a feed.
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
if !c.store.FeedExists(userID, feedID) {
response.JSON().NotFound(errors.New("Unable to find this feed"))
return
}
err = c.feedHandler.RefreshFeed(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to refresh this feed"))
return
}
response.JSON().NoContent()
}
// UpdateFeed is the API handler that is used to update a feed.
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
newFeed, err := decodeFeedModificationPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return
}
originalFeed, err := c.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().NotFound(errors.New("Unable to find this feed"))
return
}
if originalFeed == nil {
response.JSON().NotFound(errors.New("Feed not found"))
return
}
originalFeed.Merge(newFeed)
if err := c.store.UpdateFeed(originalFeed); err != nil {
response.JSON().ServerError(errors.New("Unable to update this feed"))
return
}
originalFeed, err = c.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
return
}
response.JSON().Created(originalFeed)
}
// GetFeeds is the API handler that get all feeds that belongs to the given user.
func (c *Controller) GetFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
feeds, err := c.store.Feeds(ctx.UserID())
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch feeds from the database"))
return
}
response.JSON().Standard(feeds)
}
// Export is the API handler that incoves an OPML export.
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
opmlHandler := opml.NewHandler(c.store)
opml, err := opmlHandler.Export(ctx.LoggedUser().ID)
if err != nil {
response.JSON().ServerError(errors.New("unable to export feeds to OPML"))
}
response.XML().Serve(opml)
}
// GetFeed is the API handler to get a feed.
func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
feed, err := c.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
return
}
if feed == nil {
response.JSON().NotFound(errors.New("Feed not found"))
return
}
response.JSON().Standard(feed)
}
// RemoveFeed is the API handler to remove a feed.
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
if !c.store.FeedExists(userID, feedID) {
response.JSON().NotFound(errors.New("Feed not found"))
return
}
if err := c.store.RemoveFeed(userID, feedID); err != nil {
response.JSON().ServerError(errors.New("Unable to remove this feed"))
return
}
response.JSON().NoContent()
}

43
api/icon.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"github.com/miniflux/miniflux/http/handler"
)
// FeedIcon returns a feed icon.
func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
if !c.store.HasIcon(feedID) {
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
return
}
icon, err := c.store.IconByFeedID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
return
}
if icon == nil {
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
return
}
response.JSON().Standard(&feedIcon{
ID: icon.ID,
MimeType: icon.MimeType,
Data: icon.DataURL(),
})
}

101
api/payload.go Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"encoding/json"
"fmt"
"io"
"github.com/miniflux/miniflux/model"
)
type feedIcon struct {
ID int64 `json:"id"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type entriesResponse struct {
Total int `json:"total"`
Entries model.Entries `json:"entries"`
}
func decodeUserPayload(data io.Reader) (*model.User, error) {
var user model.User
decoder := json.NewDecoder(data)
if err := decoder.Decode(&user); err != nil {
return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
}
return &user, nil
}
func decodeURLPayload(data io.Reader) (string, error) {
type payload struct {
URL string `json:"url"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return "", fmt.Errorf("invalid JSON payload: %v", err)
}
return p.URL, nil
}
func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
type payload struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
}
return p.EntryIDs, p.Status, nil
}
func decodeFeedCreationPayload(data io.Reader) (string, int64, error) {
type payload struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
}
return p.FeedURL, p.CategoryID, nil
}
func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
var feed model.Feed
decoder := json.NewDecoder(data)
if err := decoder.Decode(&feed); err != nil {
return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
}
return &feed, nil
}
func decodeCategoryPayload(data io.Reader) (*model.Category, error) {
var category model.Category
decoder := json.NewDecoder(data)
if err := decoder.Decode(&category); err != nil {
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
}
return &category, nil
}

35
api/subscription.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"fmt"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/reader/subscription"
)
// GetSubscriptions is the API handler to find subscriptions.
func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
websiteURL, err := decodeURLPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
subscriptions, err := subscription.FindSubscriptions(websiteURL)
if err != nil {
response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
return
}
if subscriptions == nil {
response.JSON().NotFound(fmt.Errorf("No subscription found"))
return
}
response.JSON().Standard(subscriptions)
}

185
api/user.go Normal file
View File

@ -0,0 +1,185 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"errors"
"github.com/miniflux/miniflux/http/handler"
)
// CreateUser is the API handler to create a new user.
func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
user, err := decodeUserPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
if err := user.ValidateUserCreation(); err != nil {
response.JSON().BadRequest(err)
return
}
if c.store.UserExists(user.Username) {
response.JSON().BadRequest(errors.New("This user already exists"))
return
}
err = c.store.CreateUser(user)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this user"))
return
}
user.Password = ""
response.JSON().Created(user)
}
// UpdateUser is the API handler to update the given user.
func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
userID, err := request.IntegerParam("userID")
if err != nil {
response.JSON().BadRequest(err)
return
}
user, err := decodeUserPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
return
}
if err := user.ValidateUserModification(); err != nil {
response.JSON().BadRequest(err)
return
}
originalUser, err := c.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
return
}
if originalUser == nil {
response.JSON().NotFound(errors.New("User not found"))
return
}
originalUser.Merge(user)
if err = c.store.UpdateUser(originalUser); err != nil {
response.JSON().ServerError(errors.New("Unable to update this user"))
return
}
response.JSON().Created(originalUser)
}
// Users is the API handler to get the list of users.
func (c *Controller) Users(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
users, err := c.store.Users()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of users"))
return
}
response.JSON().Standard(users)
}
// UserByID is the API handler to fetch the given user by the ID.
func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
userID, err := request.IntegerParam("userID")
if err != nil {
response.JSON().BadRequest(err)
return
}
user, err := c.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
return
}
response.JSON().Standard(user)
}
// UserByUsername is the API handler to fetch the given user by the username.
func (c *Controller) UserByUsername(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
username := request.StringParam("username", "")
user, err := c.store.UserByUsername(username)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
return
}
response.JSON().Standard(user)
}
// RemoveUser is the API handler to remove an existing user.
func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
userID, err := request.IntegerParam("userID")
if err != nil {
response.JSON().BadRequest(err)
return
}
user, err := c.store.UserByID(userID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this user from the database"))
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
return
}
if err := c.store.RemoveUser(user.ID); err != nil {
response.JSON().BadRequest(errors.New("Unable to remove this user from the database"))
return
}
response.JSON().NoContent()
}

27
cli/ask_credentials.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"bufio"
"fmt"
"os"
"strings"
"golang.org/x/crypto/ssh/terminal"
)
func askCredentials() (string, string) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter Username: ")
username, _ := reader.ReadString('\n')
fmt.Print("Enter Password: ")
bytePassword, _ := terminal.ReadPassword(0)
fmt.Printf("\n")
return strings.TrimSpace(username), strings.TrimSpace(string(bytePassword))
}

64
cli/cli.go Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"flag"
"fmt"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/daemon"
"github.com/miniflux/miniflux/storage"
"github.com/miniflux/miniflux/version"
)
// Parse parses command line arguments.
func Parse() {
flagInfo := flag.Bool("info", false, "Show application information")
flagVersion := flag.Bool("version", false, "Show application version")
flagMigrate := flag.Bool("migrate", false, "Migrate database schema")
flagFlushSessions := flag.Bool("flush-sessions", false, "Flush all sessions (disconnect users)")
flagCreateAdmin := flag.Bool("create-admin", false, "Create admin user")
flagResetPassword := flag.Bool("reset-password", false, "Reset user password")
flag.Parse()
cfg := config.NewConfig()
store := storage.NewStorage(
cfg.DatabaseURL(),
cfg.DatabaseMaxConnections(),
)
if *flagInfo {
info()
return
}
if *flagVersion {
fmt.Println(version.Version)
return
}
if *flagMigrate {
store.Migrate()
return
}
if *flagFlushSessions {
flushSessions(store)
return
}
if *flagCreateAdmin {
createAdmin(store)
return
}
if *flagResetPassword {
resetPassword(store)
return
}
daemon.Run(cfg, store)
}

34
cli/create_admin.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"fmt"
"os"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/storage"
)
func createAdmin(store *storage.Storage) {
user := model.NewUser()
user.Username = os.Getenv("ADMIN_USERNAME")
user.Password = os.Getenv("ADMIN_PASSWORD")
user.IsAdmin = true
if user.Username == "" || user.Password == "" {
user.Username, user.Password = askCredentials()
}
if err := user.ValidateUserCreation(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if err := store.CreateUser(user); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

10
cli/doc.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package cli implements command line arguments for Miniflux application.
*/
package cli

20
cli/flush_sessions.go Normal file
View File

@ -0,0 +1,20 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"fmt"
"os"
"github.com/miniflux/miniflux/storage"
)
func flushSessions(store *storage.Storage) {
fmt.Println("Flushing all sessions (disconnect users)")
if err := store.FlushAllSessions(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

18
cli/info.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"fmt"
"runtime"
"github.com/miniflux/miniflux/version"
)
func info() {
fmt.Println("Version:", version.Version)
fmt.Println("Build Date:", version.BuildDate)
fmt.Println("Go Version:", runtime.Version())
}

39
cli/reset_password.go Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
import (
"fmt"
"os"
"github.com/miniflux/miniflux/storage"
)
func resetPassword(store *storage.Storage) {
username, password := askCredentials()
user, err := store.UserByUsername(username)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if user == nil {
fmt.Println("User not found!")
os.Exit(1)
}
user.Password = password
if err := user.ValidatePassword(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if err := store.UpdateUser(user); err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Password changed!")
}

View File

@ -1,56 +0,0 @@
Miniflux API Client
===================
[![PkgGoDev](https://pkg.go.dev/badge/miniflux.app/v2/client)](https://pkg.go.dev/miniflux.app/v2/client)
Client library for Miniflux REST API.
Installation
------------
```bash
go get -u miniflux.app/v2/client
```
Example
-------
```go
package main
import (
"fmt"
"os"
miniflux "miniflux.app/v2/client"
)
func main() {
// Authentication with username/password:
client := miniflux.New("https://api.example.org", "admin", "secret")
// Authentication with an API Key:
client := miniflux.New("https://api.example.org", "my-secret-token")
// Fetch all feeds.
feeds, err := client.Feeds()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(feeds)
// Backup your feeds to an OPML file.
opml, err := client.Export()
if err != nil {
fmt.Println(err)
return
}
err = os.WriteFile("opml.xml", opml, 0644)
if err != nil {
fmt.Println(err)
return
}
}
```

View File

@ -1,696 +0,0 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"encoding/json"
"fmt"
"io"
"net/url"
"strconv"
"strings"
)
// Client holds API procedure calls.
type Client struct {
request *request
}
// New returns a new Miniflux client.
// Deprecated: use NewClient instead.
func New(endpoint string, credentials ...string) *Client {
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")
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}}
}
}
// 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.
func (c *Client) Me() (*User, error) {
body, err := c.request.Get("/v1/me")
if err != nil {
return nil, err
}
defer body.Close()
var user *User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: json error (%v)", err)
}
return user, nil
}
// Users returns all users.
func (c *Client) Users() (Users, error) {
body, err := c.request.Get("/v1/users")
if err != nil {
return nil, err
}
defer body.Close()
var users Users
if err := json.NewDecoder(body).Decode(&users); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return users, nil
}
// UserByID returns a single user.
func (c *Client) UserByID(userID int64) (*User, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/users/%d", userID))
if err != nil {
return nil, err
}
defer body.Close()
var user User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &user, nil
}
// UserByUsername returns a single user.
func (c *Client) UserByUsername(username string) (*User, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/users/%s", username))
if err != nil {
return nil, err
}
defer body.Close()
var user User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &user, nil
}
// CreateUser creates a new user in the system.
func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
body, err := c.request.Post("/v1/users", &UserCreationRequest{
Username: username,
Password: password,
IsAdmin: isAdmin,
})
if err != nil {
return nil, err
}
defer body.Close()
var user *User
if err := json.NewDecoder(body).Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return user, nil
}
// UpdateUser updates a user in the system.
func (c *Client) UpdateUser(userID int64, userChanges *UserModificationRequest) (*User, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", userID), userChanges)
if err != nil {
return nil, err
}
defer body.Close()
var u *User
if err := json.NewDecoder(body).Decode(&u); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return u, nil
}
// DeleteUser removes a user from the system.
func (c *Client) DeleteUser(userID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
}
// MarkAllAsRead marks all unread entries as read for a given user.
func (c *Client) MarkAllAsRead(userID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
return err
}
// Discover try to find subscriptions from a website.
func (c *Client) Discover(url string) (Subscriptions, error) {
body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
if err != nil {
return nil, err
}
defer body.Close()
var subscriptions Subscriptions
if err := json.NewDecoder(body).Decode(&subscriptions); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return subscriptions, nil
}
// Categories gets the list of categories.
func (c *Client) Categories() (Categories, error) {
body, err := c.request.Get("/v1/categories")
if err != nil {
return nil, err
}
defer body.Close()
var categories Categories
if err := json.NewDecoder(body).Decode(&categories); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return categories, nil
}
// CreateCategory creates a new category.
func (c *Client) CreateCategory(title string) (*Category, error) {
body, err := c.request.Post("/v1/categories", map[string]interface{}{
"title": title,
})
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// UpdateCategory updates a category.
func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
"title": title,
})
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
return err
}
// CategoryFeeds gets feeds of a category.
func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/feeds", categoryID))
if err != nil {
return nil, err
}
defer body.Close()
var feeds Feeds
if err := json.NewDecoder(body).Decode(&feeds); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feeds, nil
}
// DeleteCategory removes a category.
func (c *Client) DeleteCategory(categoryID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
}
// RefreshCategory refreshes a category.
func (c *Client) RefreshCategory(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/refresh", categoryID), nil)
return err
}
// Feeds gets all feeds.
func (c *Client) Feeds() (Feeds, error) {
body, err := c.request.Get("/v1/feeds")
if err != nil {
return nil, err
}
defer body.Close()
var feeds Feeds
if err := json.NewDecoder(body).Decode(&feeds); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feeds, nil
}
// Export creates OPML file.
func (c *Client) Export() ([]byte, error) {
body, err := c.request.Get("/v1/export")
if err != nil {
return nil, err
}
defer body.Close()
opml, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return opml, nil
}
// Import imports an OPML file.
func (c *Client) Import(f io.ReadCloser) error {
_, err := c.request.PostFile("/v1/import", f)
return err
}
// Feed gets a feed.
func (c *Client) Feed(feedID int64) (*Feed, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID))
if err != nil {
return nil, err
}
defer body.Close()
var feed *Feed
if err := json.NewDecoder(body).Decode(&feed); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feed, nil
}
// CreateFeed creates a new feed.
func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) (int64, error) {
body, err := c.request.Post("/v1/feeds", feedCreationRequest)
if err != nil {
return 0, err
}
defer body.Close()
type result struct {
FeedID int64 `json:"feed_id"`
}
var r result
if err := json.NewDecoder(body).Decode(&r); err != nil {
return 0, fmt.Errorf("miniflux: response error (%v)", err)
}
return r.FeedID, nil
}
// UpdateFeed updates a feed.
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feedID), feedChanges)
if err != nil {
return nil, err
}
defer body.Close()
var f *Feed
if err := json.NewDecoder(body).Decode(&f); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return f, nil
}
// MarkFeedAsRead marks all unread entries of the feed as read.
func (c *Client) MarkFeedAsRead(feedID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
return err
}
// RefreshAllFeeds refreshes all feeds.
func (c *Client) RefreshAllFeeds() error {
_, err := c.request.Put("/v1/feeds/refresh", nil)
return err
}
// RefreshFeed refreshes a feed.
func (c *Client) RefreshFeed(feedID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
return err
}
// DeleteFeed removes a feed.
func (c *Client) DeleteFeed(feedID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
}
// FeedIcon gets a feed icon.
func (c *Client) FeedIcon(feedID int64) (*FeedIcon, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d/icon", feedID))
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
}
// FeedEntry gets a single feed entry.
func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d/entries/%d", feedID, entryID))
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
}
// CategoryEntry gets a single category entry.
func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID))
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
}
// Entry gets a single entry.
func (c *Client) Entry(entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID))
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
}
// Entries fetch entries.
func (c *Client) Entries(filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString("/v1/entries", filter)
body, err := c.request.Get(path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// FeedEntries fetch feed entries.
func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString(fmt.Sprintf("/v1/feeds/%d/entries", feedID), filter)
body, err := c.request.Get(path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// CategoryEntries fetch entries of a category.
func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter)
body, err := c.request.Get(path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// UpdateEntries updates the status of a list of entries.
func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
type payload struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}
_, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
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)
return err
}
// SaveEntry sends an entry to a third-party service.
func (c *Client) SaveEntry(entryID int64) error {
_, err := c.request.Post(fmt.Sprintf("/v1/entries/%d/save", entryID), nil)
return err
}
// 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 {
return nil, err
}
defer body.Close()
var result FeedCounters
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
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{}
if filter.Status != "" {
values.Set("status", filter.Status)
}
if filter.Direction != "" {
values.Set("direction", filter.Direction)
}
if filter.Order != "" {
values.Set("order", filter.Order)
}
if filter.Limit >= 0 {
values.Set("limit", strconv.Itoa(filter.Limit))
}
if filter.Offset >= 0 {
values.Set("offset", strconv.Itoa(filter.Offset))
}
if filter.After > 0 {
values.Set("after", strconv.FormatInt(filter.After, 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))
}
if filter.Starred != "" {
values.Set("starred", filter.Starred)
}
if filter.Search != "" {
values.Set("search", filter.Search)
}
if filter.CategoryID > 0 {
values.Set("category_id", strconv.FormatInt(filter.CategoryID, 10))
}
if filter.FeedID > 0 {
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
}
for _, status := range filter.Statuses {
values.Add("status", status)
}
path = fmt.Sprintf("%s?%s", path, values.Encode())
}
return path
}

View File

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
/*
Package client implements a client library for the Miniflux REST API.
# Examples
This code snippet fetch the list of users:
import (
miniflux "miniflux.app/v2/client"
)
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
users, err := client.Users()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(users, err)
This one discover subscriptions on a website:
subscriptions, err := client.Discover("https://example.org/")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(subscriptions)
*/
package client // import "miniflux.app/v2/client"

View File

@ -1,298 +0,0 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"fmt"
"time"
)
// Entry statuses.
const (
EntryStatusUnread = "unread"
EntryStatusRead = "read"
EntryStatusRemoved = "removed"
)
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
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"`
LastLoginAt *time.Time `json:"last_login_at"`
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"`
}
func (u User) String() string {
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
}
// UserCreationRequest represents the request to create a user.
type UserCreationRequest struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
}
// 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"`
MediaPlaybackRate *float64 `json:"media_playback_rate"`
}
// Users represents a list of users.
type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
func (c Category) String() string {
return fmt.Sprintf("#%d %s", c.ID, c.Title)
}
// Categories represents a list of categories.
type Categories []*Category
// Subscription represents a feed subscription.
type Subscription struct {
Title string `json:"title"`
URL string `json:"url"`
Type string `json:"type"`
}
func (s Subscription) String() string {
return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
}
// Subscriptions represents a list of subscriptions.
type Subscriptions []*Subscription
// Feed represents a Miniflux feed.
type Feed struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at,omitempty"`
EtagHeader string `json:"etag_header,omitempty"`
LastModifiedHeader string `json:"last_modified_header,omitempty"`
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
Crawler bool `json:"crawler"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
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.
type FeedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
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.
type FeedModificationRequest struct {
FeedURL *string `json:"feed_url"`
SiteURL *string `json:"site_url"`
Title *string `json:"title"`
ScraperRules *string `json:"scraper_rules"`
RewriteRules *string `json:"rewrite_rules"`
BlocklistRules *string `json:"blocklist_rules"`
KeeplistRules *string `json:"keeplist_rules"`
Crawler *bool `json:"crawler"`
UserAgent *string `json:"user_agent"`
Cookie *string `json:"cookie"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
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.
type FeedIcon struct {
ID int64 `json:"id"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type FeedCounters struct {
ReadCounters map[int64]int `json:"reads"`
UnreadCounters map[int64]int `json:"unreads"`
}
// Feeds represents a list of feeds.
type Feeds []*Feed
// Entry represents a subscription item in the system.
type Entry struct {
ID int64 `json:"id"`
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"`
URL string `json:"url"`
CommentsURL string `json:"comments_url"`
Title string `json:"title"`
Status string `json:"status"`
Content string `json:"content"`
Author string `json:"author"`
ShareCode string `json:"share_code"`
Enclosures Enclosures `json:"enclosures,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.
type Entries []*Entry
// Enclosure represents an attachment.
type Enclosure struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
EntryID int64 `json:"entry_id"`
URL string `json:"url"`
MimeType string `json:"mime_type"`
Size int `json:"size"`
}
// Enclosures represents a list of attachments.
type Enclosures []*Enclosure
const (
FilterNotStarred = "0"
FilterOnlyStarred = "1"
)
// 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
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.
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
}

View File

@ -1,167 +0,0 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package client // import "miniflux.app/v2/client"
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
)
const (
userAgent = "Miniflux Client Library"
defaultTimeout = 80
)
// List of exposed errors.
var (
ErrNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
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 {
ErrorMessage string `json:"error_message"`
}
type request struct {
endpoint string
username string
password string
apiKey string
}
func (r *request) Get(path string) (io.ReadCloser, error) {
return r.execute(http.MethodGet, path, nil)
}
func (r *request) Post(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, data)
}
func (r *request) PostFile(path string, f io.ReadCloser) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, f)
}
func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPut, path, data)
}
func (r *request) Delete(path string) error {
_, err := r.execute(http.MethodDelete, path, nil)
return err
}
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
if r.endpoint[len(r.endpoint)-1:] == "/" {
r.endpoint = r.endpoint[:len(r.endpoint)-1]
}
u, err := url.Parse(r.endpoint + path)
if err != nil {
return nil, err
}
request := &http.Request{
URL: u,
Method: method,
Header: r.buildHeaders(),
}
if r.username != "" && r.password != "" {
request.SetBasicAuth(r.username, r.password)
}
if data != nil {
switch data := data.(type) {
case io.ReadCloser:
request.Body = data
default:
request.Body = io.NopCloser(bytes.NewBuffer(r.toJSON(data)))
}
}
client := r.buildClient()
response, err := client.Do(request)
if err != nil {
return nil, err
}
switch response.StatusCode {
case http.StatusUnauthorized:
response.Body.Close()
return nil, ErrNotAuthorized
case http.StatusForbidden:
response.Body.Close()
return nil, ErrForbidden
case http.StatusInternalServerError:
defer response.Body.Close()
var resp errorResponse
decoder := json.NewDecoder(response.Body)
// If we failed to decode, just return a generic ErrServerError
if err := decoder.Decode(&resp); err != nil {
return nil, ErrServerError
}
return nil, errors.New("miniflux: internal server error: " + resp.ErrorMessage)
case http.StatusNotFound:
response.Body.Close()
return nil, ErrNotFound
case http.StatusNoContent:
response.Body.Close()
return nil, nil
case http.StatusBadRequest:
defer response.Body.Close()
var resp errorResponse
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&resp); err != nil {
return nil, fmt.Errorf("%w (%v)", ErrBadRequest, err)
}
return nil, fmt.Errorf("%w (%s)", ErrBadRequest, resp.ErrorMessage)
}
if response.StatusCode > 400 {
response.Body.Close()
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
}
return response.Body, nil
}
func (r *request) buildClient() http.Client {
return http.Client{
Timeout: time.Duration(defaultTimeout * time.Second),
}
}
func (r *request) buildHeaders() http.Header {
headers := make(http.Header)
headers.Add("User-Agent", userAgent)
headers.Add("Content-Type", "application/json")
headers.Add("Accept", "application/json")
if r.apiKey != "" {
headers.Add("X-Auth-Token", r.apiKey)
}
return headers
}
func (r *request) toJSON(v interface{}) []byte {
b, err := json.Marshal(v)
if err != nil {
log.Println("Unable to convert interface to JSON:", err)
return []byte("")
}
return b
}

139
config/config.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config
import (
"os"
"strconv"
)
const (
defaultBaseURL = "http://localhost"
defaultDatabaseURL = "postgres://postgres:postgres@localhost/miniflux2?sslmode=disable"
defaultWorkerPoolSize = 5
defaultPollingFrequency = 60
defaultBatchSize = 10
defaultDatabaseMaxConns = 20
defaultListenAddr = "127.0.0.1:8080"
defaultCertFile = ""
defaultKeyFile = ""
defaultCertDomain = ""
defaultCertCache = "/tmp/cert_cache"
defaultSessionCleanupFrequency = 24
)
// Config manages configuration parameters.
type Config struct {
IsHTTPS bool
}
func (c *Config) get(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func (c *Config) getInt(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
v, _ := strconv.Atoi(value)
return v
}
// BaseURL returns the application base URL.
func (c *Config) BaseURL() string {
return c.get("BASE_URL", defaultBaseURL)
}
// DatabaseURL returns the database URL.
func (c *Config) DatabaseURL() string {
return c.get("DATABASE_URL", defaultDatabaseURL)
}
// DatabaseMaxConnections returns the number of maximum database connections.
func (c *Config) DatabaseMaxConnections() int {
return c.getInt("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
}
// ListenAddr returns the listen address for the HTTP server.
func (c *Config) ListenAddr() string {
return c.get("LISTEN_ADDR", defaultListenAddr)
}
// CertFile returns the SSL certificate filename if any.
func (c *Config) CertFile() string {
return c.get("CERT_FILE", defaultCertFile)
}
// KeyFile returns the private key filename for custom SSL certificate.
func (c *Config) KeyFile() string {
return c.get("KEY_FILE", defaultKeyFile)
}
// CertDomain returns the domain to use for Let's Encrypt certificate.
func (c *Config) CertDomain() string {
return c.get("CERT_DOMAIN", defaultCertDomain)
}
// CertCache returns the directory to use for Let's Encrypt session cache.
func (c *Config) CertCache() string {
return c.get("CERT_CACHE", defaultCertCache)
}
// SessionCleanupFrequency returns the interval for session cleanup.
func (c *Config) SessionCleanupFrequency() int {
return c.getInt("SESSION_CLEANUP_FREQUENCY", defaultSessionCleanupFrequency)
}
// WorkerPoolSize returns the number of background worker.
func (c *Config) WorkerPoolSize() int {
return c.getInt("WORKER_POOL_SIZE", defaultWorkerPoolSize)
}
// PollingFrequency returns the interval to refresh feeds in the background.
func (c *Config) PollingFrequency() int {
return c.getInt("POLLING_FREQUENCY", defaultPollingFrequency)
}
// BatchSize returns the number of feeds to send for background processing.
func (c *Config) BatchSize() int {
return c.getInt("BATCH_SIZE", defaultBatchSize)
}
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (c *Config) IsOAuth2UserCreationAllowed() bool {
return c.getInt("OAUTH2_USER_CREATION", 0) == 1
}
// OAuth2ClientID returns the OAuth2 Client ID.
func (c *Config) OAuth2ClientID() string {
return c.get("OAUTH2_CLIENT_ID", "")
}
// OAuth2ClientSecret returns the OAuth2 client secret.
func (c *Config) OAuth2ClientSecret() string {
return c.get("OAUTH2_CLIENT_SECRET", "")
}
// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (c *Config) OAuth2RedirectURL() string {
return c.get("OAUTH2_REDIRECT_URL", "")
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (c *Config) OAuth2Provider() string {
return c.get("OAUTH2_PROVIDER", "")
}
// NewConfig returns a new Config.
func NewConfig() *Config {
return &Config{IsHTTPS: os.Getenv("HTTPS") != ""}
}

10
config/doc.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package config handles configuration values for Miniflux application.
*/
package config

View File

@ -1,4 +0,0 @@
The contrib directory contains various useful things contributed by the community.
Community contributions are not officially supported by the maintainers.
There is no guarantee whatsoever that anything in this folder works.

View File

@ -1,8 +0,0 @@
---
miniflux_linux_user: miniflux
miniflux_db_user_name: miniflux_db_user
miniflux_db_user_password: miniflux_db_user_password
miniflux_db: miniflux_db
miniflux_admin_name: admin
miniflux_admin_passwort: miniflux_admin_password
miniflux_port: 8080

View File

@ -1,4 +0,0 @@
---
- hosts: miniflux
roles:
- { role: mgrote.miniflux, tags: "miniflux" }

View File

@ -1,23 +0,0 @@
## mgrote.miniflux
### Details
Installs and configures Miniflux v2 with ansible
### Works on...
- [x] Ubuntu (>=18.04)
### Variables and Defaults
##### Linux User
miniflux_linux_user: miniflux
##### DB User
miniflux_db_user_name: miniflux_db_user
##### DB Password
miniflux_db_user_password: qqqqqqqqqqqqq
##### Database
miniflux_db: miniflux_db
##### Username Miniflux Admin
miniflux_admin_name: admin
##### Password Miniflux Admin
miniflux_admin_passwort: hallowelt
##### Port for Miniflux Frontend
miniflux_port: 8080

View File

@ -1,11 +0,0 @@
---
- name: start_miniflux.service
become: yes
systemd:
name: miniflux
state: restarted
enabled: yes
# wait 15 seconds(for systemd)
- name: miniflux_wait
wait_for:
timeout: 15

View File

@ -1,43 +0,0 @@
- name: add Apt-key for miniflux-repo
become: yes
apt_key:
url: https://apt.miniflux.app/KEY.gpg
state: present
- name: add miniflux-repo
become: yes
apt_repository:
repo: 'deb https://apt.miniflux.app/ /'
state: present
filename: miniflux_repo
update_cache: yes
- name: install miniflux
become: yes
apt:
name: miniflux
state: present
- name: add miniflux linux_user
become: yes
user:
name: "{{ miniflux_linux_user }}"
home: "/var/empty"
create_home: "no"
system: "yes"
shell: "/bin/false"
- name: create directory "/etc/miniflux.d"
become: yes
file:
path: /etc/miniflux.d
state: directory
- name: copy miniflux.conf
become: yes
template:
src: "miniflux.conf"
dest: "/etc/miniflux.conf"
notify:
- start_miniflux.service
- miniflux_wait

View File

@ -1,18 +0,0 @@
# See https://docs.miniflux.app/
LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
POLLING_FREQUENCY=15
PROXY_IMAGES=http-only
# Run SQL migrations automatically:
RUN_MIGRATIONS=1
CREATE_ADMIN=1
ADMIN_USERNAME={{ miniflux_admin_name }}
ADMIN_PASSWORD={{ miniflux_admin_passwort }}
POLLING_FREQUENCY=10
# Options: https://miniflux.app/miniflux.1.html

View File

@ -1,6 +0,0 @@
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

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,23 +0,0 @@
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"
}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,27 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,16 +0,0 @@
meta {
name: Get all categories
type: http
seq: 9
}
get {
url: {{minifluxBaseURL}}/v1/categories
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,16 +0,0 @@
meta {
name: Get all users
type: http
seq: 2
}
get {
url: {{minifluxBaseURL}}/v1/users
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,27 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,16 +0,0 @@
meta {
name: Get current user
type: http
seq: 1
}
get {
url: {{minifluxBaseURL}}/v1/me
body: none
auth: basic
}
auth:basic {
username: {{minifluxUsername}}
password: {{minifluxPassword}}
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,16 +0,0 @@
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}}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,40 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,22 +0,0 @@
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"
}
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,26 +0,0 @@
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
}

View File

@ -1,23 +0,0 @@
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"
}
}

View File

@ -1,27 +0,0 @@
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
}

View File

@ -1,5 +0,0 @@
{
"version": "1",
"name": "Miniflux",
"type": "collection"
}

View File

@ -1,7 +0,0 @@
vars {
minifluxBaseURL: http://127.0.0.1:8080
minifluxUsername: admin
}
vars:secret [
minifluxPassword
]

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