Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
173
.all-contributorsrc
Normal file
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "nicholas-fedor",
|
||||
"name": "Nicholas Fedor",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/71477161?v=4",
|
||||
"profile": "https://github.com/nicholas-fedor",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "amirschnell",
|
||||
"name": "Amir Schnell",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/9380508?v=4",
|
||||
"profile": "https://github.com/amirschnell",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "piksel",
|
||||
"name": "nils måsén",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
|
||||
"profile": "https://piksel.se",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lukapeschke",
|
||||
"name": "Luka Peschke",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/17085536?v=4",
|
||||
"profile": "https://github.com/lukapeschke",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MrLuje",
|
||||
"name": "MrLuje",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/632075?v=4",
|
||||
"profile": "https://github.com/MrLuje",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simskij",
|
||||
"name": "Simon Aronsson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
|
||||
"profile": "http://simme.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "arnested",
|
||||
"name": "Arne Jørgensen",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/190005?v=4",
|
||||
"profile": "https://arnested.dk",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "atighineanu",
|
||||
"name": "Alexei Tighineanu",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/27206712?v=4",
|
||||
"profile": "https://github.com/atighineanu",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ellisab",
|
||||
"name": "Alexandru Bonini",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1402047?v=4",
|
||||
"profile": "https://github.com/ellisab",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sentriz",
|
||||
"name": "Senan Kelly",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/6832539?v=4",
|
||||
"profile": "https://senan.xyz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JonasPf",
|
||||
"name": "JonasPf",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2216775?v=4",
|
||||
"profile": "https://github.com/JonasPf",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "claycooper",
|
||||
"name": "claycooper",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3612906?v=4",
|
||||
"profile": "https://github.com/claycooper",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "darktohka",
|
||||
"name": "Derzsi Dániel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16326697?v=4",
|
||||
"profile": "http://ko-fi.com/disyer",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JosephKav",
|
||||
"name": "Joseph Kavanagh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4267227?v=4",
|
||||
"profile": "https://josephkav.io",
|
||||
"contributions": [
|
||||
"code",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "justinsteven",
|
||||
"name": "Justin Steven",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1893909?v=4",
|
||||
"profile": "https://ring0.lol",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "serverleader",
|
||||
"name": "Carlos Savcic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34089?v=4",
|
||||
"profile": "https://github.com/serverleader",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "shoutrrr",
|
||||
"projectOwner": "nicholas-fedor",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true,
|
||||
"commitType": "docs",
|
||||
"commitConvention": "angular"
|
||||
}
|
48
.circleci/config.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
||||
# See: https://circleci.com/docs/configuration-reference
|
||||
|
||||
# For a detailed guide to building and testing with Go, read the docs:
|
||||
# https://circleci.com/docs/language-go/ for more details
|
||||
version: 2.1
|
||||
|
||||
# Define a job to be invoked later in a workflow.
|
||||
# See: https://circleci.com/docs/jobs-steps/#jobs-overview & https://circleci.com/docs/configuration-reference/#jobs
|
||||
jobs:
|
||||
build:
|
||||
# Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
|
||||
# See: https://circleci.com/docs/executor-intro/ & https://circleci.com/docs/configuration-reference/#executor-job
|
||||
docker:
|
||||
# Specify the version you desire here
|
||||
# See: https://circleci.com/developer/images/image/cimg/go
|
||||
- image: cimg/go:1.24.2@sha256:cd027ede83e11c7b1002dfff3f4975fbf0124c5028df4c63da571c30db88fb3c
|
||||
|
||||
# Add steps to the job
|
||||
# See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps
|
||||
steps:
|
||||
# Checkout the code as the first step.
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-v4-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: go mod download
|
||||
- save_cache:
|
||||
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "/go/pkg/mod"
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
mkdir -p /tmp/test-reports
|
||||
gotestsum --junitfile /tmp/test-reports/unit-tests.xml
|
||||
- store_test_results:
|
||||
path: /tmp/test-reports
|
||||
|
||||
# Orchestrate jobs using workflows
|
||||
# See: https://circleci.com/docs/workflows/ & https://circleci.com/docs/configuration-reference/#workflows
|
||||
workflows:
|
||||
build-test: # This is the name of the workflow, feel free to change it to better match your workflow.
|
||||
# Inside the workflow, you define the jobs you want to run.
|
||||
jobs:
|
||||
- build
|
6
.codacy.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
engines:
|
||||
coverage:
|
||||
exclude_paths:
|
||||
- "*.md"
|
||||
- "**/*.md"
|
16
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
|
||||
Thank you for contributing to the shoutrrr project! 🙏
|
||||
|
||||
We truly appreciate all the contributions we get from the community.
|
||||
To make your PR experience as smooth as possible, make sure that you
|
||||
include the following in your PR:
|
||||
|
||||
- What your PR contributes
|
||||
- Which issues it solves (preferrably using auto closing instructions like "closes #123".
|
||||
- Tests that verify the code your contributing
|
||||
- Updates to the documentation
|
||||
|
||||
Thank you again! ✨
|
||||
|
||||
-->
|
66
.github/workflows/build.yaml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
snapshot:
|
||||
description: "Whether to run in snapshot mode"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e
|
||||
with:
|
||||
go-version: 1.24.3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@6d4b68b490aef8836e8fb5e50ee7b3bdfa5894f0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@6d4b68b490aef8836e8fb5e50ee7b3bdfa5894f0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@90c43f2c197eeb47adb636c4329af34ae5a2a5f0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v2.7.0
|
||||
args: release --clean ${{ inputs.snapshot && '--snapshot' || '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2
|
||||
if: success()
|
||||
with:
|
||||
subject-path: "dist/**/*"
|
32
.github/workflows/clean-cache.yaml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Cache cleanup
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh cache list --ref $BRANCH --limit 100 --json id --jq '.[].id')
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh cache delete $cacheKey
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
|
56
.github/workflows/docs.yaml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: Publish Docs
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
|
||||
- name: Configure Git Credentials
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Generate Service Config Docs
|
||||
run: |
|
||||
go mod download
|
||||
go clean -cache # Clear build cache
|
||||
./generate-service-config-docs.sh
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
with:
|
||||
python-version: "3.13.3"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
docs-requirements.txt
|
||||
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: .cache
|
||||
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
|
||||
- name: Install mkdocs
|
||||
run: |
|
||||
pip install -r docs-requirements.txt
|
||||
|
||||
- name: Build and Deploy
|
||||
run: mkdocs gh-deploy --force --verbose
|
36
.github/workflows/lint.yaml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
name: Lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e
|
||||
with:
|
||||
go-version: "1.24.3"
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@4d56fa9e3c67fb4afa92b38c99fc7f20f5eeff4e
|
||||
with:
|
||||
args: --timeout=5m --config= # Use default linter settings
|
||||
|
||||
- name: Format Go code
|
||||
run: |
|
||||
go fmt ./...
|
||||
|
||||
- name: Check for uncommitted changes after formatting
|
||||
run: |
|
||||
git diff --exit-code || (echo "Detected unformatted files. Run 'go fmt' to format your code."; exit 1)
|
20
.github/workflows/pull-request.yaml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Pull Request
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yaml
|
||||
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
32
.github/workflows/release-dev.yaml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Push to main
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags-ignore:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- "docs/*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yaml
|
||||
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
build-and-publish:
|
||||
uses: ./.github/workflows/build.yaml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- test
|
||||
with:
|
||||
snapshot: true
|
37
.github/workflows/release-production.yaml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: Release (Production)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yaml
|
||||
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
build:
|
||||
uses: ./.github/workflows/build.yaml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- test
|
||||
|
||||
renew-docs:
|
||||
name: Refresh pkg.go.dev
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Pull new module version
|
||||
uses: nicholas-fedor/go-proxy-pull-action@ad5d0f8b44e5478055cf78227eb300d2b02786f2
|
||||
with:
|
||||
goproxy: https://proxy.golang.org
|
||||
import_path: github.com/nicholas-fedor/shoutrrr
|
32
.github/workflows/test.yaml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests and collect coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e
|
||||
with:
|
||||
go-version: "1.24.3"
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
28
.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Binaries for programs and plugins
|
||||
# ---
|
||||
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/shoutrrr/shoutrrr
|
||||
*.snap
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
# ---
|
||||
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
# ---
|
||||
|
||||
*.out
|
||||
.idea
|
||||
|
||||
report.json
|
||||
coverage.txt
|
||||
*.coverprofile
|
||||
dist
|
||||
site
|
||||
docs/services/*/config.md
|
634
.golangci.yaml
Normal file
|
@ -0,0 +1,634 @@
|
|||
######################################################################################################
|
||||
# #
|
||||
# Shoutrrr golangci-lint Configuration #
|
||||
# #
|
||||
# Shoutrrr: https://github.com/nicholas-fedor/shoutrrr/ #
|
||||
# Golangci-lint: https://golangci-lint.run/ #
|
||||
# #
|
||||
######################################################################################################
|
||||
|
||||
version: "2"
|
||||
|
||||
######################################################################################################
|
||||
# Linters Configuration
|
||||
# https://golangci-lint.run/usage/linters/
|
||||
######################################################################################################
|
||||
linters:
|
||||
####################################################################################################
|
||||
# Default set of linters.
|
||||
# The value can be: `standard`, `all`, `none`, or `fast`.
|
||||
# Default: standard
|
||||
# default: all
|
||||
|
||||
####################################################################################################
|
||||
enable:
|
||||
##################################################################################################
|
||||
# Enabled linters that automatically resolve issues
|
||||
- canonicalheader # Canonicalheader checks whether net/http.Header uses canonical header.
|
||||
- copyloopvar # A linter detects places where loop variables are copied.
|
||||
- dupword # Checks for duplicate words in the source code.
|
||||
- err113 # Go linter to check the errors handling expressions.
|
||||
- errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
|
||||
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
|
||||
- fatcontext # Detects nested contexts in loops and function literals.
|
||||
- ginkgolinter # Enforces standards of using ginkgo and gomega.
|
||||
- gocritic # Provides diagnostics that check for bugs, performance and style issues.
|
||||
- godot # Check if comments end in a period.
|
||||
- goheader # Checks if file header matches to pattern.
|
||||
- importas # Enforces consistent import aliases.
|
||||
- intrange # Intrange is a linter to find places where for loops could make use of an integer range.
|
||||
- mirror # Reports wrong mirror patterns of bytes/strings usage.
|
||||
- misspell # Finds commonly misspelled English words.
|
||||
- nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero).
|
||||
- nlreturn # Nlreturn checks for a new line before return and branch statements to increase code clarity.
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives.
|
||||
- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative.
|
||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
|
||||
- staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint.
|
||||
- tagalign # Check that struct tags are well aligned.
|
||||
- testifylint # Checks usage of github.com/stretchr/testify.
|
||||
- usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library.
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
||||
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
|
||||
- wsl # Add or remove empty lines.
|
||||
##################################################################################################
|
||||
# Enabled linters that require manual issue resolution
|
||||
- asasalint # Check for pass []any as any in variadic func(...any).
|
||||
- asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name.
|
||||
- bidichk # Checks for dangerous unicode character sequences.
|
||||
- bodyclose # Checks whether HTTP response body is closed successfully.
|
||||
- containedctx # Containedctx is a linter that detects struct contained context.Context field.
|
||||
- contextcheck # Check whether the function uses a non-inherited context.
|
||||
- decorder # Check declaration order and count of types, constants, variables and functions.
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()).
|
||||
- dupl # Detects duplicate fragments of code.
|
||||
- durationcheck # Check for two durations multiplied together.
|
||||
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occurrences where the check for the returned error can be omitted.
|
||||
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
|
||||
- exhaustive # Check exhaustiveness of enum switch statements.
|
||||
- forbidigo # Forbids identifiers.
|
||||
- forcetypeassert # Finds forced type assertions.
|
||||
- gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid.
|
||||
- gochecksumtype # Run exhaustiveness checks on Go "sum types".
|
||||
- goconst # Finds repeated strings that could be replaced by a constant.
|
||||
- godox # Detects usage of FIXME, TODO and other keywords inside comments.
|
||||
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
|
||||
- goprintffuncname # Checks that printf-like functions are named with `f` at the end.
|
||||
- gosec # Inspects source code for security problems.
|
||||
- grouper # Analyze expression groups.
|
||||
- iface # Detect the incorrect use of interfaces, helping developers avoid interface pollution.
|
||||
- inamedparam # Reports interfaces with unnamed method parameters.
|
||||
- loggercheck # Checks key value pairs for common logger libraries (kitlog,klog,logr,zap).
|
||||
- makezero # Finds slice declarations with non-zero initial length.
|
||||
- mnd # An analyzer to detect magic numbers.
|
||||
- musttag # Enforce field tags in (un)marshaled structs.
|
||||
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
|
||||
- nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error.
|
||||
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
|
||||
- noctx # Finds sending http request without context.Context.
|
||||
- nonamedreturns # Reports all named returns.
|
||||
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
|
||||
- prealloc # Finds slice declarations that could potentially be pre-allocated.
|
||||
- predeclared # Find code that shadows one of Go's predeclared identifiers.
|
||||
- promlinter # Check Prometheus metrics naming via promlint.
|
||||
- reassign # Checks that package variables are not reassigned.
|
||||
- recvcheck # Checks for receiver type consistency.
|
||||
- spancheck # Checks for mistakes with OpenTelemetry/Census spans.
|
||||
- sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed.
|
||||
- thelper # Thelper detects tests helpers which is not start with t.Helper() method.
|
||||
- tparallel # Tparallel detects inappropriate usage of t.Parallel() method in your Go test codes.
|
||||
- unconvert # Remove unnecessary type conversions.
|
||||
- unparam # Reports unused function parameters.
|
||||
- varnamelen # Checks that the length of a variable's name matches its scope.
|
||||
- wastedassign # Finds wasted assignment statements.
|
||||
- wrapcheck # Checks that errors returned from external packages are wrapped.
|
||||
disable:
|
||||
- cyclop # Checks function and package cyclomatic complexity.
|
||||
- depguard # Checks if package imports are in a list of acceptable packages.
|
||||
- exhaustruct # Checks if all structure fields are initialized.
|
||||
- funlen # Checks for long functions.
|
||||
- gochecknoinits # Checks that no init functions are present in Go code.
|
||||
- gochecknoglobals # Check that no global variables exist.
|
||||
- gocognit # Computes and checks the cognitive complexity of functions.
|
||||
- gocyclo # Computes and checks the cyclomatic complexity of functions. [fast]
|
||||
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast]
|
||||
- gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase.
|
||||
- interfacebloat # A linter that checks the number of methods inside an interface. [fast]
|
||||
- ireturn # Accept Interfaces, Return Concrete Types.
|
||||
- lll # Reports long lines.
|
||||
- maintidx # Maintidx measures the maintainability index of each function. [fast]
|
||||
- nestif # Reports deeply nested if statements.
|
||||
- rowserrcheck # Checks whether Rows.Err of rows is checked successfully.
|
||||
- paralleltest # Detects missing usage of t.Parallel() method in your Go test.
|
||||
- protogetter # Reports direct reads from proto message fields when getters should be used. [auto-fix]
|
||||
- sloglint # Ensure consistent code style when using log/slog.
|
||||
- tagliatelle # Checks the struct tags.
|
||||
- testableexamples # Linter checks if examples are testable (have an expected output). [fast]
|
||||
- testpackage # Linter that makes you use a separate _test package.
|
||||
- zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg`.
|
||||
|
||||
######################################################################################################
|
||||
# Linter Settings Configuration
|
||||
######################################################################################################
|
||||
settings:
|
||||
varnamelen:
|
||||
max-distance: 5
|
||||
min-name-length: 3
|
||||
check-return: true
|
||||
check-type-param: true
|
||||
ignore-type-assert-ok: true
|
||||
ignore-map-index-ok: true
|
||||
ignore-chan-recv-ok: true
|
||||
ignore-names:
|
||||
- err
|
||||
- c
|
||||
- ctx
|
||||
- i
|
||||
- j
|
||||
ignore-decls:
|
||||
- c echo.Context
|
||||
- t testing.T
|
||||
- f *foo.Bar
|
||||
- e error
|
||||
- i int
|
||||
- j int
|
||||
- const C
|
||||
- T any
|
||||
- m map[string]int
|
||||
- w http.ResponseWriter
|
||||
- r *http.Request
|
||||
- r http.Request
|
||||
- r *net/http/Request
|
||||
- r *mux.Router
|
||||
|
||||
######################################################################################################
|
||||
# Defines a set of rules to ignore issues.
|
||||
# It does not skip the analysis, and so does not ignore "typecheck" errors.
|
||||
exclusions:
|
||||
# Mode of the generated files analysis.
|
||||
#
|
||||
# - `strict`: sources are excluded by strictly following the Go generated file convention.
|
||||
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
|
||||
# This line must appear before the first non-comment, non-blank text in the file.
|
||||
# https://go.dev/s/generatedcode
|
||||
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
|
||||
# - `disable`: disable the generated files exclusion.
|
||||
#
|
||||
# Default: lax
|
||||
# generated: strict
|
||||
|
||||
####################################################################################################
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
|
||||
####################################################################################################
|
||||
# Predefined exclusion rules.
|
||||
# Default: []
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
- legacy
|
||||
|
||||
####################################################################################################
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source.
|
||||
rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: ".*_test.go$"
|
||||
linters:
|
||||
- dupl
|
||||
- err113
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- forcetypeassert
|
||||
- gocyclo
|
||||
- gosec
|
||||
- promlinter
|
||||
- wrapcheck
|
||||
- varnamelen
|
||||
|
||||
# Run some linter only for test files by excluding its issues for everything else.
|
||||
# - path-except: _test\.go
|
||||
# linters:
|
||||
# - forbidigo
|
||||
|
||||
# Exclude known linters from partially hard-vendored code,
|
||||
# which is impossible to exclude via `nolint` comments.
|
||||
# `/` will be replaced by the current OS file path separator to properly work on Windows.
|
||||
# - path: internal/hmac/
|
||||
# text: "weak cryptographic primitive"
|
||||
# linters:
|
||||
# - gosec
|
||||
|
||||
# Exclude some `staticcheck` messages.
|
||||
# - linters:
|
||||
# - staticcheck
|
||||
# text: "SA9003:"
|
||||
|
||||
# Exclude `lll` issues for long lines with `go:generate`.
|
||||
# - linters:
|
||||
# - lll
|
||||
# source: "^//go:generate "
|
||||
|
||||
####################################################################################################
|
||||
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
|
||||
# "/" will be replaced by the current OS file path separator to properly work on Windows.
|
||||
# Default: []
|
||||
# paths:
|
||||
# - ".*\\.my\\.go$"
|
||||
# - lib/bad.go
|
||||
# - ".*/mocks/.*"
|
||||
# - third_party$
|
||||
# - builtin$
|
||||
# - examples$
|
||||
|
||||
####################################################################################################
|
||||
# Which file paths to not exclude.
|
||||
# Default: []
|
||||
# paths-except:
|
||||
# - ".*\\.my\\.go$"
|
||||
# - lib/bad.go
|
||||
|
||||
######################################################################################################
|
||||
# Formatters Configuration
|
||||
# https://golangci-lint.run/usage/configuration/#formatters-configuration
|
||||
######################################################################################################
|
||||
|
||||
formatters:
|
||||
# Enable specific formatter.
|
||||
# Default: []
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- golines
|
||||
|
||||
####################################################################################################
|
||||
# Formatters settings.
|
||||
settings:
|
||||
gci:
|
||||
# Section configuration to compare against.
|
||||
# Section names are case-insensitive and may contain parameters in ().
|
||||
# The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`,
|
||||
# If `custom-order` is `true`, it follows the order of `sections` option.
|
||||
# Default: ["standard", "default"]
|
||||
sections:
|
||||
- standard # Standard section: captures all standard packages.
|
||||
- default # Default section: contains all imports that could not be matched to another section type.
|
||||
- prefix(github.com/nicholas-fedor/watchtower) # Custom section: groups all imports with the specified Prefix.
|
||||
- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled.
|
||||
- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled.
|
||||
- alias # Alias section: contains all alias imports. This section is not present unless explicitly enabled.
|
||||
- localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled.
|
||||
# Checks that no inline comments are present.
|
||||
# Default: false
|
||||
# no-inline-comments: true
|
||||
# Checks that no prefix comments (comment lines above an import) are present.
|
||||
# Default: false
|
||||
# no-prefix-comments: true
|
||||
# Enable custom order of sections.
|
||||
# If `true`, make the section order the same as the order of `sections`.
|
||||
# Default: false
|
||||
# custom-order: true
|
||||
# Drops lexical ordering for custom sections.
|
||||
# Default: false
|
||||
# no-lex-order: true
|
||||
gofmt:
|
||||
# Simplify code: gofmt with `-s` option.
|
||||
# Default: true
|
||||
# simplify: false
|
||||
# Apply the rewrite rules to the source before reformatting.
|
||||
# https://pkg.go.dev/cmd/gofmt
|
||||
# Default: []
|
||||
rewrite-rules:
|
||||
- pattern: "interface{}"
|
||||
replacement: "any"
|
||||
- pattern: "a[b:len(a)]"
|
||||
replacement: "a[b:]"
|
||||
|
||||
####################################################################################################
|
||||
# exclusions:
|
||||
# Mode of the generated files analysis.
|
||||
#
|
||||
# - `strict`: sources are excluded by strictly following the Go generated file convention.
|
||||
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
|
||||
# This line must appear before the first non-comment, non-blank text in the file.
|
||||
# https://go.dev/s/generatedcode
|
||||
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
|
||||
# - `disable`: disable the generated files exclusion.
|
||||
#
|
||||
# Default: lax
|
||||
# generated: strict
|
||||
# Which file paths to exclude.
|
||||
# Default: []
|
||||
# paths:
|
||||
# - ".*mocks$"
|
||||
# - third_party$
|
||||
# - builtin$
|
||||
# - examples$
|
||||
|
||||
######################################################################################################
|
||||
# Issues Configuration
|
||||
# https://golangci-lint.run/usage/configuration/#issues-configuration
|
||||
######################################################################################################
|
||||
issues:
|
||||
# Maximum issues count per one linter.
|
||||
# Set to 0 to disable.
|
||||
# Default: 50
|
||||
max-issues-per-linter: 0
|
||||
|
||||
####################################################################################################
|
||||
# Maximum count of issues with the same text.
|
||||
# Set to 0 to disable.
|
||||
# Default: 3
|
||||
max-same-issues: 0
|
||||
|
||||
####################################################################################################
|
||||
# Make issues output unique by line.
|
||||
# Default: true
|
||||
# uniq-by-line: false
|
||||
|
||||
####################################################################################################
|
||||
# Show only new issues: if there are unstaged changes or untracked files,
|
||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||
# It's a super-useful option for integration of golangci-lint into existing large codebase.
|
||||
# It's not practical to fix all existing issues at the moment of integration:
|
||||
# much better don't allow issues in new code.
|
||||
#
|
||||
# Default: false
|
||||
# new: true
|
||||
|
||||
####################################################################################################
|
||||
# Show only new issues created after the best common ancestor (merge-base against HEAD).
|
||||
# Default: ""
|
||||
# new-from-merge-base: main
|
||||
|
||||
####################################################################################################
|
||||
# Show only new issues created after git revision `REV`.
|
||||
# Default: ""
|
||||
# new-from-rev: HEAD
|
||||
|
||||
####################################################################################################
|
||||
# Show only new issues created in git patch with set file path.
|
||||
# Default: ""
|
||||
# new-from-patch: path/to/patch/file
|
||||
|
||||
####################################################################################################
|
||||
# Show issues in any part of update files (requires new-from-rev or new-from-patch).
|
||||
# Default: false
|
||||
whole-files: true
|
||||
|
||||
####################################################################################################
|
||||
# Fix found issues (if it's supported by the linter).
|
||||
# Default: false
|
||||
fix: true
|
||||
|
||||
######################################################################################################
|
||||
# Output Configuration
|
||||
# https://golangci-lint.run/usage/configuration/#output-configuration
|
||||
######################################################################################################
|
||||
output:
|
||||
####################################################################################################
|
||||
# The formats used to render issues.
|
||||
formats:
|
||||
##################################################################################################
|
||||
# Prints issues in a text format with colors, line number, and linter name.
|
||||
# This format is the default format.
|
||||
text:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.txt
|
||||
|
||||
# Print linter name in the end of issue text.
|
||||
# Default: true
|
||||
# print-linter-name: false
|
||||
print-linter-name: true
|
||||
|
||||
# Print lines of code with issue.
|
||||
# Default: true
|
||||
# print-issued-lines: false
|
||||
print-issued-lines: true
|
||||
|
||||
# Use colors.
|
||||
# Default: true
|
||||
# colors: false
|
||||
colors: true
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in a JSON representation.
|
||||
# json:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.json
|
||||
# path: stderr
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in columns representation separated by tabulations.
|
||||
tab:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.txt
|
||||
# Print linter name in the end of issue text.
|
||||
# Default: true
|
||||
# print-linter-name: true
|
||||
# Use colors.
|
||||
# Default: true
|
||||
# colors: false
|
||||
colors: true
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in an HTML page.
|
||||
# It uses the Cloudflare CDN (cdnjs) and React.
|
||||
# html:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.html
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in the Checkstyle format.
|
||||
# checkstyle:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.xml
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in the Code Climate format.
|
||||
# code-climate:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.json
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in the JUnit XML format.
|
||||
# junit-xml:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.xml
|
||||
# Support extra JUnit XML fields.
|
||||
# Default: false
|
||||
# extended: true
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in the TeamCity format.
|
||||
# teamcity:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.txt
|
||||
|
||||
##################################################################################################
|
||||
# Prints issues in the SARIF format.
|
||||
# sarif:
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Default: stdout
|
||||
# path: ./path/to/output.json
|
||||
|
||||
####################################################################################################
|
||||
# Add a prefix to the output file references.
|
||||
# Default: ""
|
||||
# path-prefix: ""
|
||||
|
||||
####################################################################################################
|
||||
# Order to use when sorting results.
|
||||
# Possible values: `file`, `linter`, and `severity`.
|
||||
#
|
||||
# If the severity values are inside the following list, they are ordered in this order:
|
||||
# 1. error
|
||||
# 2. warning
|
||||
# 3. high
|
||||
# 4. medium
|
||||
# 5. low
|
||||
# Either they are sorted alphabetically.
|
||||
#
|
||||
# Default: ["linter", "file"]
|
||||
sort-order:
|
||||
- file # filepath, line, and column.
|
||||
- severity
|
||||
- linter
|
||||
|
||||
####################################################################################################
|
||||
# Show statistics per linter.
|
||||
# Default: true
|
||||
# show-stats: false
|
||||
|
||||
######################################################################################################
|
||||
# Run Configuration
|
||||
# Options for analysis running.
|
||||
# https://golangci-lint.run/usage/configuration/#run-configuration
|
||||
######################################################################################################
|
||||
run:
|
||||
####################################################################################################
|
||||
# Timeout for analysis, e.g. 30s, 5m, 5m30s.
|
||||
# If the value is lower or equal to 0, the timeout is disabled.
|
||||
# Default: 1m
|
||||
# timeout: 5m
|
||||
|
||||
####################################################################################################
|
||||
# The mode used to evaluate relative paths.
|
||||
# It's used by exclusions, Go plugins, and some linters.
|
||||
# The value can be:
|
||||
# - `gomod`: the paths will be relative to the directory of the `go.mod` file.
|
||||
# - `gitroot`: the paths will be relative to the git root (the parent directory of `.git`).
|
||||
# - `cfg`: the paths will be relative to the configuration file.
|
||||
# - `wd` (NOT recommended): the paths will be relative to the place where golangci-lint is run.
|
||||
# Default: wd
|
||||
# relative-path-mode: gomod
|
||||
|
||||
####################################################################################################
|
||||
# Exit code when at least one issue was found.
|
||||
# Default: 1
|
||||
# issues-exit-code: 2
|
||||
|
||||
####################################################################################################
|
||||
# Include test files or not.
|
||||
# Default: true
|
||||
# tests: false
|
||||
|
||||
####################################################################################################
|
||||
# List of build tags, all linters use it.
|
||||
# Default: []
|
||||
# build-tags:
|
||||
# - mytag
|
||||
|
||||
####################################################################################################
|
||||
# If set, we pass it to "go list -mod={option}". From "go help modules":
|
||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||
# to go.mod are needed. This setting is most useful to check that go.mod does
|
||||
# not need updates, such as in a continuous integration and testing system.
|
||||
# If invoked with -mod=vendor, the go command assumes that the vendor
|
||||
# directory holds the correct copies of dependencies and ignores
|
||||
# the dependency descriptions in go.mod.
|
||||
#
|
||||
# Allowed values: readonly|vendor|mod
|
||||
# Default: ""
|
||||
# modules-download-mode: readonly
|
||||
|
||||
####################################################################################################
|
||||
# Allow multiple parallel golangci-lint instances running.
|
||||
# If false, golangci-lint acquires file lock on start.
|
||||
# Default: false
|
||||
allow-parallel-runners: true
|
||||
|
||||
####################################################################################################
|
||||
# Allow multiple golangci-lint instances running, but serialize them around a lock.
|
||||
# If false, golangci-lint exits with an error if it fails to acquire file lock on start.
|
||||
# Default: false
|
||||
allow-serial-runners: true
|
||||
|
||||
####################################################################################################
|
||||
# Define the Go version limit.
|
||||
# Mainly related to generics support since go1.18.
|
||||
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.22.
|
||||
# go: "1.24"
|
||||
|
||||
####################################################################################################
|
||||
# Number of operating system threads (`GOMAXPROCS`) that can execute golangci-lint simultaneously.
|
||||
# If it is explicitly set to 0 (i.e. not the default) then golangci-lint will automatically set the value to match Linux container CPU quota.
|
||||
# Default: the number of logical CPUs in the machine
|
||||
# concurrency: 4
|
||||
######################################################################################################
|
||||
# Severity Configuration
|
||||
# https://golangci-lint.run/usage/configuration/#severity-configuration
|
||||
######################################################################################################
|
||||
# severity:
|
||||
####################################################################################################
|
||||
# Set the default severity for issues.
|
||||
#
|
||||
# If severity rules are defined and the issues do not match or no severity is provided to the rule
|
||||
# this will be the default severity applied.
|
||||
# Severities should match the supported severity names of the selected out format.
|
||||
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
|
||||
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel
|
||||
# - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||
# - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance
|
||||
#
|
||||
# `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...)
|
||||
#
|
||||
# Default: ""
|
||||
# default: error
|
||||
|
||||
####################################################################################################
|
||||
# If set to true `severity-rules` regular expressions become case-sensitive.
|
||||
# Default: false
|
||||
# case-sensitive: true
|
||||
|
||||
####################################################################################################
|
||||
# When a list of severity rules are provided, severity information will be added to lint issues.
|
||||
# Severity rules have the same filtering capability as exclude rules
|
||||
# except you are allowed to specify one matcher per severity rule.
|
||||
#
|
||||
# `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...)
|
||||
#
|
||||
# Only affects out formats that support setting severity information.
|
||||
#
|
||||
# Default: []
|
||||
# rules:
|
||||
# - linters:
|
||||
# - dupl
|
||||
# severity: info
|
||||
######################################################################################################
|
||||
# End of golangci-lint Configuration
|
||||
######################################################################################################
|
36
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
## Prerequisites
|
||||
|
||||
To contribute code changes to this project you will need to install the go distribution.
|
||||
|
||||
* [Go](https://golang.org/doc/install)
|
||||
|
||||
Also, as shoutrrr utilizes go modules for vendor locking, you'll need at least Go 1.24.
|
||||
You can check your current version of the go language as follows:
|
||||
|
||||
```bash
|
||||
~ $ go version
|
||||
go version go1.24.0 windows/amd64
|
||||
```
|
||||
|
||||
## Checking out the code
|
||||
|
||||
Do not place your code in the go source path.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:<yourfork>/shoutrrr.git
|
||||
cd shoutrrr
|
||||
```
|
||||
|
||||
## Building and testing
|
||||
|
||||
shoutrrr is a go library and is built with go commands. The following commands assume that you are at the root level of your repo.
|
||||
|
||||
```bash
|
||||
./build.sh # compiles and packages an executable stand-alone client of shoutrrr
|
||||
go test ./... -v # runs tests with verbose output
|
||||
./shoutrrr/shoutrrr # runs the application
|
||||
```
|
||||
|
||||
## Commit messages
|
||||
|
||||
Shoutrrr try to follow the conventional commit specification. More information is available [here](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
|
21
LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Nicholas Fedor
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
158
README.md
Normal file
|
@ -0,0 +1,158 @@
|
|||
<div align="center">
|
||||
|
||||
<a href="https://github.com/nicholas-fedor/shoutrrr">
|
||||
<img src="https://raw.githubusercontent.com/nicholas-fedor/shoutrrr/main/docs/shoutrrr-logotype.png" width="450" />
|
||||
</a>
|
||||
|
||||
# Shoutrrr
|
||||
|
||||
Notification library for gophers and their furry friends.
|
||||
Heavily inspired by <a href="https://github.com/caronc/apprise">caronc/apprise</a>.
|
||||
|
||||

|
||||
[](https://codecov.io/gh/nicholas-fedor/shoutrrr)
|
||||
[](https://www.codacy.com/gh/nicholas-fedor/shoutrrr/dashboard?utm_source=github.com&utm_medium=referral&utm_content=nicholas-fedor/shoutrrr&utm_campaign=Badge_Grade)
|
||||
[](https://goreportcard.com/badge/github.com/nicholas-fedor/shoutrrr)
|
||||
[](https://pkg.go.dev/github.com/nicholas-fedor/shoutrrr)
|
||||
[](https://github.com/nicholas-fedor/shoutrrr)
|
||||
[](https://github.com/nicholas-fedor/shoutrrr/blob/main/LICENSE)
|
||||
[](https://hub.docker.com/r/nickfedor/shoutrrr)
|
||||
[](https://godoc.org/github.com/nicholas-fedor/shoutrrr) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
</div>
|
||||
<br/><br/>
|
||||
|
||||
## Installation
|
||||
|
||||
### Using the Go CLI
|
||||
|
||||
```bash
|
||||
go install github.com/nicholas-fedor/shoutrrr/shoutrrr@latest
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
go build -o shoutrrr ./shoutrrr
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### As a package
|
||||
|
||||
Using shoutrrr is easy! There is currently two ways of using it as a package.
|
||||
|
||||
#### Using the direct send command
|
||||
|
||||
```go
|
||||
url := "slack://token-a/token-b/token-c"
|
||||
err := shoutrrr.Send(url, "Hello world (or slack channel) !")
|
||||
|
||||
```
|
||||
|
||||
#### Using a sender
|
||||
|
||||
```go
|
||||
url := "slack://token-a/token-b/token-c"
|
||||
sender, err := shoutrrr.CreateSender(url)
|
||||
sender.Send("Hello world (or slack channel) !", map[string]string { /* ... */ })
|
||||
```
|
||||
|
||||
#### Using a sender with multiple URLs
|
||||
|
||||
```go
|
||||
urls := []string {
|
||||
"slack://token-a/token-b/token-c"
|
||||
"discord://token@channel"
|
||||
}
|
||||
sender, err := shoutrrr.CreateSender(urls...)
|
||||
sender.Send("Hello world (or slack channel) !", map[string]string { /* ... */ })
|
||||
```
|
||||
|
||||
### Through the CLI
|
||||
|
||||
Start by running the `build.sh` script.
|
||||
You may then run send notifications using the shoutrrr executable:
|
||||
|
||||
```shell
|
||||
shoutrrr send [OPTIONS] <URL> <Message [...]>
|
||||
```
|
||||
|
||||
### From a GitHub Actions workflow
|
||||
|
||||
You can also use Shoutrrr from a GitHub Actions workflow.
|
||||
|
||||
See this example and the [action on GitHub
|
||||
Marketplace](https://github.com/marketplace/actions/shoutrrr-action):
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Some other steps needed for deploying
|
||||
run: ...
|
||||
- name: Shoutrrr
|
||||
uses: nicholas-fedor/shoutrrr-action@v1
|
||||
with:
|
||||
url: ${{ secrets.SHOUTRRR_URL }}
|
||||
title: Deployed ${{ github.sha }}
|
||||
message: See changes at ${{ github.event.compare }}.
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For additional details, visit the [full documentation](https://nicholas-fedor.github.io/shoutrrr).
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-fedor"><img src="https://avatars2.githubusercontent.com/u/71477161?v=4?s=100" width="100px;" alt="Nicholas Fedor"/><br /><sub><b>Nicholas Fedor</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=nicholas-fedor" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=nicholas-fedor" title="Documentation">📖</a> <a href="#maintenance-nicholas-fedor" title="Maintenance">🚧</a> <a href="https://github.com/nicholas-fedor/shoutrrr/pulls?q=is%3Apr+reviewed-by%3Anicholas-fedor" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amirschnell"><img src="https://avatars3.githubusercontent.com/u/9380508?v=4?s=100" width="100px;" alt="Amir Schnell"/><br /><sub><b>Amir Schnell</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=amirschnell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt="nils måsén"/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=piksel" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=piksel" title="Documentation">📖</a> <a href="#maintenance-piksel" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=lukapeschke" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrLuje"><img src="https://avatars0.githubusercontent.com/u/632075?v=4?s=100" width="100px;" alt="MrLuje"/><br /><sub><b>MrLuje</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=MrLuje" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=MrLuje" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=simskij" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=simskij" title="Documentation">📖</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt="Arne Jørgensen"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=arnested" title="Documentation">📖</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=arnested" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/atighineanu"><img src="https://avatars1.githubusercontent.com/u/27206712?v=4?s=100" width="100px;" alt="Alexei Tighineanu"/><br /><sub><b>Alexei Tighineanu</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=atighineanu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ellisab"><img src="https://avatars2.githubusercontent.com/u/1402047?v=4?s=100" width="100px;" alt="Alexandru Bonini"/><br /><sub><b>Alexandru Bonini</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=ellisab" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://senan.xyz"><img src="https://avatars0.githubusercontent.com/u/6832539?v=4?s=100" width="100px;" alt="Senan Kelly"/><br /><sub><b>Senan Kelly</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=sentriz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JonasPf"><img src="https://avatars.githubusercontent.com/u/2216775?v=4?s=100" width="100px;" alt="JonasPf"/><br /><sub><b>JonasPf</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=JonasPf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/claycooper"><img src="https://avatars.githubusercontent.com/u/3612906?v=4?s=100" width="100px;" alt="claycooper"/><br /><sub><b>claycooper</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=claycooper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ko-fi.com/disyer"><img src="https://avatars.githubusercontent.com/u/16326697?v=4?s=100" width="100px;" alt="Derzsi Dániel"/><br /><sub><b>Derzsi Dániel</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=darktohka" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://josephkav.io"><img src="https://avatars.githubusercontent.com/u/4267227?v=4?s=100" width="100px;" alt="Joseph Kavanagh"/><br /><sub><b>Joseph Kavanagh</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=JosephKav" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/issues?q=author%3AJosephKav" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt="Justin Steven"/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/issues?q=author%3Ajustinsteven" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/serverleader"><img src="https://avatars.githubusercontent.com/u/34089?v=4?s=100" width="100px;" alt="Carlos Savcic"/><br /><sub><b>Carlos Savcic</b></sub></a><br /><a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=serverleader" title="Code">💻</a> <a href="https://github.com/nicholas-fedor/shoutrrr/commits?author=serverleader" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
## Related Project(s)
|
||||
|
||||
- [watchtower](https://github.com/nicholas-fedor/watchtower) - automate Docker container image updates
|
14
SECURITY.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Shoutrrr v0.x strives to be fully backwards-compatible, and hence only the latest minor will get security updates unless explicitly requested.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.6.x | :white_check_mark: |
|
||||
| < 0.6 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Vulnerabilities can be disclosed via email to <nick@nickfedor.com>
|
3
build.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
go build -o shoutrrr/ ./shoutrrr
|
12
dockerfiles/Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
|||
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c AS alpine
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=alpine \
|
||||
/etc/ssl/certs/ca-certificates.crt \
|
||||
/etc/ssl/certs/ca-certificates.crt
|
||||
COPY shoutrrr/shoutrrr /
|
||||
|
||||
ENTRYPOINT ["/shoutrrr"]
|
4
docs-requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
mkdocs
|
||||
mkdocs-material
|
||||
mkdocs-git-revision-date-localized-plugin
|
||||
mkdocs-minify-plugin
|
19
docs/examples/generic.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Examples
|
||||
|
||||
Examples of service URLs that can be used with [the generic service](../../services/generic) together with common service providers.
|
||||
|
||||
## Home Assistant
|
||||
|
||||
The service URL needs to be:
|
||||
```
|
||||
generic://HAIPAddress:HAPort/api/webhook/WebhookIDFromHA?template=json
|
||||
```
|
||||
|
||||
And, if you need http://
|
||||
```
|
||||
generic://HAIPAddress:HAPort/api/webhook/WebhookIDFromHA?template=json&disabletls=yes
|
||||
```
|
||||
|
||||
Then, in HA, use `{{ trigger.json.message }}` to get the message sent from the JSON.
|
||||
|
||||
_Credit [@JeffCrum1](https://github.com/JeffCrum1), source: [https://github.com/nicholas-fedor/shoutrrr/issues/325#issuecomment-1460105065]_
|
BIN
docs/favicon.ico
Normal file
After Width: | Height: | Size: 31 KiB |
20
docs/generators/basic.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Basic generator
|
||||
|
||||
The basic generator looks at the `key:""`, `desc:""` and `default:""` tags on service configuration structs and uses them to ask the user to fill in their corresponding values.
|
||||
|
||||
Example:
|
||||
```shell
|
||||
$ shoutrrr generate telegram
|
||||
```
|
||||
```yaml
|
||||
Generating URL for telegram using basic generator
|
||||
Enter the configuration values as prompted
|
||||
|
||||
Token: 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
|
||||
Preview[Yes]: No
|
||||
Notification[Yes]:
|
||||
ParseMode[None]:
|
||||
Channels: @mychannel
|
||||
|
||||
URL: telegram://110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw@telegram?channels=@mychannel¬ification=Yes&parsemode=None&preview=No
|
||||
```
|
10
docs/generators/overview.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Generators
|
||||
|
||||
Generators are used to create service configurations via the command line.
|
||||
The main generator is the reflection based [Basic generator](./basic) that aims to be able to generator configurations for all the core services via a set of simple questions.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
$ shoutrrr generate [OPTIONS] -g <GENERATOR> <SERVICE>
|
||||
```
|
156
docs/getting-started.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Getting started
|
||||
|
||||
## As a package
|
||||
|
||||
Using shoutrrr is easy! There is currently two ways of using it as a package.
|
||||
|
||||
### Using the direct send command
|
||||
Easiest to use, but very limited.
|
||||
|
||||
```go
|
||||
url := "slack://token-a/token-b/token-c"
|
||||
err := shoutrrr.Send(url, "Hello world (or slack channel) !")
|
||||
```
|
||||
|
||||
### Using a sender
|
||||
Using a sender gives you the ability to preconfigure multiple notification services and send to all of them with the same `Send(message, params)` method.
|
||||
|
||||
```go
|
||||
urlA := "slack://token-a/token-b/token-c"
|
||||
urlB := "telegram://110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw@telegram?channels=@mychannel"
|
||||
sender, err := shoutrrr.CreateSender(urlA, urlB)
|
||||
|
||||
// Send notifications instantly to all services
|
||||
sender.Send("Hello world (or slack/telegram channel)!", map[string]string { "title": "He-hey~!" })
|
||||
|
||||
// ...or bundle notifications...
|
||||
func doWork() error {
|
||||
// ...and send them when leaving the scope
|
||||
defer sender.Flush(map[string]string { "title": "Work Result" })
|
||||
|
||||
sender.Enqueue("Started doing %v", stuff)
|
||||
|
||||
// Maybe get creative...?
|
||||
defer func(start time.Time) {
|
||||
sender.Enqueue("Elapsed: %v", time.Now().Sub(start))
|
||||
}(time.Now())
|
||||
|
||||
if err := doMoreWork(); err != nil {
|
||||
sender.Enqueue("Oh no! %v", err)
|
||||
|
||||
// This will send the currently queued up messages...
|
||||
return
|
||||
}
|
||||
|
||||
sender.Enqueue("Everything went very well!")
|
||||
|
||||
// ...or this:
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Through the CLI
|
||||
|
||||
Start by running the `build.sh` script.
|
||||
You may then run the shoutrrr executable:
|
||||
|
||||
```shell
|
||||
$ ./shoutrrr
|
||||
|
||||
Usage:
|
||||
./shoutrrr <ActionVerb> [...]
|
||||
Possible actions: send, verify, generate
|
||||
```
|
||||
|
||||
On a system with Go installed you can install the latest Shoutrrr CLI
|
||||
command with:
|
||||
|
||||
```shell
|
||||
go install github.com/nicholas-fedor/shoutrrr/shoutrrr@latest
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### Send
|
||||
|
||||
Send a notification using the supplied notification service url.
|
||||
|
||||
```bash
|
||||
$ shoutrrr send \
|
||||
--url "<SERVICE_URL>" \
|
||||
--message "<MESSAGE BODY>"
|
||||
```
|
||||
|
||||
#### Verify
|
||||
|
||||
Verify the validity of a notification service url.
|
||||
|
||||
```bash
|
||||
$ shoutrrr verify \
|
||||
--url "<SERVICE_URL>"
|
||||
```
|
||||
|
||||
#### Generate
|
||||
|
||||
Generate and display the configuration for a notification service url.
|
||||
|
||||
```bash
|
||||
$ shoutrrr generate [OPTIONS] <SERVICE>
|
||||
```
|
||||
|
||||
| Flags | Description |
|
||||
| ---------------------------- | ------------------------------------------------|
|
||||
| `-g, --generator string` | The generator to use (default "basic") |
|
||||
| `-p, --property stringArray` | Configuration property in key=value format |
|
||||
| `-s, --service string` | The notification service to generate a URL for |
|
||||
|
||||
**Note**: Service can either be supplied as the first argument or using the `-s` flag.
|
||||
|
||||
For more information on generators, see [Generators](./generators/overview.md).
|
||||
|
||||
### Options
|
||||
|
||||
#### Debug
|
||||
|
||||
Enables debug output from the CLI.
|
||||
|
||||
| Flags | Env. | Default | Required |
|
||||
| --------------- | ---------------- | ------- | -------- |
|
||||
| `--debug`, `-d` | `SHOUTRRR_DEBUG` | `false` | |
|
||||
|
||||
#### URL
|
||||
|
||||
The target url for the notifications generated, see [overview](./services/overview).
|
||||
|
||||
| Flags | Env. | Default | Required |
|
||||
| ------------- | -------------- | ------- | -------- |
|
||||
| `--url`, `-u` | `SHOUTRRR_URL` | N/A | ✅ |
|
||||
|
||||
## From a GitHub Actions workflow
|
||||
|
||||
You can also use Shoutrrr from a GitHub Actions workflow.
|
||||
|
||||
See this example and the [action on GitHub
|
||||
Marketplace](https://github.com/marketplace/actions/shoutrrr-action):
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Some other steps needed for deploying
|
||||
run: ...
|
||||
- name: Shoutrrr
|
||||
uses: nicholas-fedor/shoutrrr-action@v1
|
||||
with:
|
||||
url: ${{ secrets.SHOUTRRR_URL }}
|
||||
title: Deployed ${{ github.sha }}
|
||||
message: See changes at ${{ github.event.compare }}.
|
||||
```
|
BIN
docs/guides/slack/app-api-channel-details-id.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/guides/slack/app-api-copy-oauth-token.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/guides/slack/app-api-oauth-menu.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/guides/slack/app-api-select-channel.png
Normal file
After Width: | Height: | Size: 19 KiB |
47
docs/guides/slack/index.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Slack Guides
|
||||
|
||||
Guides for setting up the [Slack](../../services/slack.md) service
|
||||
|
||||
## Getting a token
|
||||
|
||||
To enable all features, either the Legacy Webhook- (deprecated and might stop working) or the bot API tokens needs to
|
||||
be used. Only use the non-legacy Webhook if you don't need to customize the bot name or icon.
|
||||
|
||||
### Bot API (preferred)
|
||||
|
||||
1. Create a new App for your bot using the [Basic app setup guide](https://api.slack.com/authentication/basics)
|
||||
2. Install the App into your workspace ([slack docs](https://api.slack.com/authentication/basics#installing)).
|
||||
3. From [Apps](https://api.slack.com/apps), select your new App and go to **Oauth & Permissions**
|
||||
<figure><img alt="Slack app management menu screenshot" src="app-api-oauth-menu.png" height="248" /></figure>
|
||||
4. Copy the Bot User OAuth Token
|
||||
<figure><img alt="Copy OAuth token screenshot" src="app-api-copy-oauth-token.png" height="209" /></figure>
|
||||
|
||||
!!! example
|
||||
Given the API token
|
||||
<pre><code><b>xoxb</b>-<b>123456789012</b>-<b>1234567890123</b>-<b>4mt0t4l1YL3g1T5L4cK70k3N</b></code></pre>
|
||||
and the channel ID `C001CH4NN3L` (obtained by using the [guide below](#getting_the_channel_id)), the Shoutrrr URL
|
||||
should look like this:
|
||||
<pre><code>slack://<b>xoxb</b>:<b>123456789012</b>-<b>1234567890123</b>-<b>4mt0t4l1YL3g1T5L4cK70k3N</b>@<b>C001CH4NN3L</b></code></pre>
|
||||
|
||||
### Webhook tokens
|
||||
|
||||
Get a Webhook URL using the legacy [WebHooks Integration](https://slack.com/apps/new/A0F7XDUAZ-incoming-webhooks),
|
||||
or by using the [Getting started with Incoming Webhooks](https://api.slack.com/messaging/webhooks#getting_started) guide and
|
||||
replace the initial `https://hooks.slack.com/services/` part of the webhook URL with `slack://hook:` to get your Shoutrrr URL.
|
||||
|
||||
!!! info "Slack Webhook URL"
|
||||
<code>https://hooks.slack.com/services/<b>T00000000</b>/<b>B00000000</b>/<b>XXXXXXXXXXXXXXXXXXXXXXXX</b></code>
|
||||
|
||||
!!! info "Shoutrrr URL"
|
||||
<code>slack://hook:<b>T00000000</b>-<b>B00000000</b>-<b>XXXXXXXXXXXXXXXXXXXXXXXX</b>@webhook</code>
|
||||
|
||||
## Getting the Channel ID
|
||||
|
||||
!!! note ""
|
||||
Only needed for API token. Use `webhook` as the channel for webhook tokens.
|
||||
|
||||
1. In the channel you wish to post to, open **Channel Details** by clicking on the channel title.
|
||||
<figure><img alt="Opening channel details screenshot" src="app-api-select-channel.png" height="270" /></figure>
|
||||
|
||||
2. Copy the Channel ID from the bottom of the popup and append it to your Shoutrrr URL
|
||||
<figure><img alt="Copy channel ID screenshot" src="app-api-channel-details-id.png" height="99" /></figure>
|
43
docs/index.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Shoutrrr
|
||||
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/nicholas-fedor/shoutrrr/main/docs/shoutrrr-logotype.png" height="450" width="450" />
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
Notification library for gophers and their furry friends.<br />
|
||||
Heavily inspired by <a href="https://github.com/caronc/apprise">caronc/apprise</a>.
|
||||
</p>
|
||||
|
||||
<p align="center" class="badges">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://github.com/nicholas-fedor/shoutrrr/workflows/Main%20Workflow/badge.svg">
|
||||
<img src="https://github.com/nicholas-fedor/shoutrrr/workflows/Main%20Workflow/badge.svg" alt="github actions workflow status">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/nicholas-fedor/shoutrrr" rel="nofollow">
|
||||
<img alt="codecov" src="https://codecov.io/gh/nicholas-fedor/shoutrrr/branch/main/graph/badge.svg">
|
||||
</a>
|
||||
<a href="https://www.codacy.com/gh/nicholas-fedor/shoutrrr/dashboard?utm_source=github.com&utm_medium=referral&utm_content=nicholas-fedor/shoutrrr&utm_campaign=Badge_Grade" rel="nofollow">
|
||||
<img alt="Codacy Badge" src="https://app.codacy.com/project/badge/Grade/47eed72de79448e2a6e297d770355544">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/badge/github.com/nicholas-fedor/shoutrrr" rel="nofollow">
|
||||
<img alt="report card" src="https://goreportcard.com/badge/github.com/nicholas-fedor/shoutrrr">
|
||||
</a>
|
||||
<a href="https://pkg.go.dev/github.com/nicholas-fedor/shoutrrr" rel="nofollow">
|
||||
<img alt="go.dev reference" src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/nickfedor/shoutrrr" rel="nofollow">
|
||||
<img alt="Pulls from DockerHub" src="https://img.shields.io/docker/pulls/nickfedor/shoutrrr.svg">
|
||||
</a>
|
||||
<a href="https://github.com/nicholas-fedor/shoutrrr">
|
||||
<img alt="github code size in bytes" src="https://img.shields.io/github/languages/code-size/nicholas-fedor/shoutrrr.svg?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/nicholas-fedor/shoutrrr/blob/main/LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/github/license/nicholas-fedor/shoutrrr.svg?style=flat-square">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
To make it easy and streamlined to consume shoutrrr regardless of the notification service you want to use,
|
||||
we've implemented a notification service url schema. To send notifications, instantiate the `ShoutrrrClient` using one of
|
||||
the service urls from the [overview](services/overview.md).
|
9
docs/overrides/main.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block outdated %}
|
||||
You're not viewing the latest version.
|
||||
<a href="{{ '../' ~ base_url }}">
|
||||
|
||||
<strong>Click here to go to latest.</strong>
|
||||
</a>
|
||||
{% endblock %}
|
21
docs/proxy.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
To use a proxy with shoutrrr, you could either set the proxy URL in the environment variable `HTTP_PROXY` or override the default HTTP client like this:
|
||||
|
||||
```go
|
||||
proxyurl, err := url.Parse("socks5://localhost:1337")
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing proxy URL: %q", err)
|
||||
}
|
||||
|
||||
http.DefaultClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyurl),
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
```
|
7
docs/services/bark.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Bark
|
||||
|
||||
Upstream docs: https://github.com/Finb/Bark
|
||||
|
||||
## URL Format
|
||||
|
||||
--8<-- "docs/services/bark/config.md"
|
52
docs/services/discord.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Discord
|
||||
|
||||
## URL Format
|
||||
|
||||
Your Discord Webhook-URL will look like this:
|
||||
|
||||
!!! info ""
|
||||
https://discord.com/api/webhooks/__`webhookid`__/__`token`__
|
||||
|
||||
The shoutrrr service URL should look like this:
|
||||
|
||||
!!! info ""
|
||||
discord://__`token`__@__`webhookid`__[?thread_id=__`threadid`__]
|
||||
|
||||
### Thread Support
|
||||
|
||||
To send messages to a specific thread in a Discord channel, include the `thread_id` query parameter in the service URL with the ID of the target thread. For example:
|
||||
|
||||
!!! info ""
|
||||
discord://__`token`__@__`webhookid`__?thread_id=123456789
|
||||
|
||||
You can obtain the `thread_id` by right-clicking a thread in Discord and selecting "Copy ID" (requires Developer Mode to be enabled in Discord settings).
|
||||
|
||||
--8<-- "docs/services/discord/config.md"
|
||||
|
||||
## Creating a webhook in Discord
|
||||
|
||||
1. Open your channel settings by first clicking on the gear icon next to the name of the channel.
|
||||

|
||||
|
||||
2. In the menu on the left, click on *Integrations*.
|
||||

|
||||
|
||||
3. In the menu on the right, click on *Create Webhook*.
|
||||

|
||||
|
||||
4. Set the name, channel, and icon to your liking and click the *Copy Webhook URL* button.
|
||||

|
||||
|
||||
5. Press the *Save Changes* button.
|
||||

|
||||
|
||||
6. Format the service URL:
|
||||
```
|
||||
https://discord.com/api/webhooks/693853386302554172/W3dE2OZz4C13_4z_uHfDOoC7BqTW288s-z1ykqI0iJnY_HjRqMGO8Sc7YDqvf_KVKjhJ
|
||||
└────────────────┘ └──────────────────────────────────────────────────────────────────┘
|
||||
webhook id token
|
||||
|
||||
discord://W3dE2OZz4C13_4z_uHfDOoC7BqTW288s-z1ykqI0iJnY_HjRqMGO8Sc7YDqvf_KVKjhJ@693853386302554172?thread_id=123456789
|
||||
└──────────────────────────────────────────────────────────────────┘ └────────────────┘ └─────────────────┘
|
||||
token webhook id thread id
|
||||
```
|
BIN
docs/services/discord/sc-1.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
docs/services/discord/sc-2.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
docs/services/discord/sc-3.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
docs/services/discord/sc-4.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
docs/services/discord/sc-5.png
Normal file
After Width: | Height: | Size: 94 KiB |
8
docs/services/email.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Email
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
smtp://__`username`__:__`password`__@__`host`__:__`port`__/?from=__`fromAddress`__&to=__`recipient1`__[,__`recipient2`__,...]
|
||||
|
||||
--8<-- "docs/services/smtp/config.md"
|
76
docs/services/generic.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Generic
|
||||
The Generic service can be used for any target that is not explicitly supported by Shoutrrr, as long as it
|
||||
supports receiving the message via a POST request.
|
||||
Usually, this requires customization on the receiving end to interpret the payload that it receives, and might
|
||||
not be a viable approach.
|
||||
|
||||
Common examples for use with service providers can be found under [examples](../examples/generic.md).
|
||||
|
||||
## Custom headers
|
||||
You can add additional HTTP headers to your request by adding query variables prefixed with `@` (`@key=value`).
|
||||
|
||||
Using
|
||||
```url
|
||||
generic://example.com?@acceptLanguage=tlh-Piqd
|
||||
```
|
||||
would result in the additional header being added:
|
||||
|
||||
```http
|
||||
Accept-Language: tlh-Piqd
|
||||
```
|
||||
|
||||
## JSON template
|
||||
By using the built in `JSON` template (`template=json`) you can create a generic JSON payload. The keys used for `title` and `message` can be overriden
|
||||
by supplying the params/query values `titleKey` and `messageKey`.
|
||||
|
||||
!!! example
|
||||
```json
|
||||
{
|
||||
"title": "Oh no!",
|
||||
"message": "The thing happened and now there is stuff all over the area!"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom data fields
|
||||
When using the JSON template, you can add additional key/value pairs to the JSON object by adding query variables prefixed with `$` (`$key=value`).
|
||||
|
||||
!!! example
|
||||
Using `generic://example.com?$projection=retroazimuthal` would yield:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Amazing opportunities!",
|
||||
"message": "New map book available for purchase.",
|
||||
"projection": "retroazimuthal"
|
||||
}
|
||||
```
|
||||
|
||||
## Shortcut URL
|
||||
You can just add `generic+` as a prefix to your target URL to use it with the generic service, so
|
||||
```url
|
||||
https://example.com/api/v1/postStuff
|
||||
```
|
||||
would become
|
||||
```url
|
||||
generic+https://example.com/api/v1/postStuff
|
||||
```
|
||||
|
||||
!!! note
|
||||
Any query variables added to the URL will be escaped so that they can be forwarded to the remote server. That means that you cannot use `?template=json` with the `generic+https://`, just use `generic://` instead!
|
||||
|
||||
## Forwarded query variables
|
||||
All query variables that are not listed in the [Query/Param Props](#queryparam_props) section will be
|
||||
forwarded to the target endpoint.
|
||||
If you need to pass a query variable that _is_ reserved, you can prefix it with an underscore (`_`).
|
||||
|
||||
!!! example
|
||||
The URL `generic+https://example.com/api/v1/postStuff?contenttype=text/plain` would send a POST message
|
||||
to `https://example.com/api/v1/postStuff` using the `Content-Type: text/plain` header.
|
||||
|
||||
If instead escaped, `generic+https://example.com/api/v1/postStuff?_contenttype=text/plain` would send a POST message
|
||||
to `https://example.com/api/v1/postStuff?contenttype=text/plain` using the `Content-Type: application/json` header (as it's the default).
|
||||
|
||||
|
||||
## URL Format
|
||||
|
||||
--8<-- "docs/services/generic/config.md"
|
37
docs/services/googlechat.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Google Chat
|
||||
|
||||
## URL Format
|
||||
|
||||
Your Google Chat Incoming Webhook URL will look like this:
|
||||
|
||||
!!! info ""
|
||||
https://chat.googleapis.com/v1/spaces/__`FOO`__/messages?key=__`bar`__&token=__`baz`__
|
||||
|
||||
The shoutrrr service URL should look like this:
|
||||
|
||||
!!! info ""
|
||||
googlechat://chat.googleapis.com/v1/spaces/__`FOO`__/messages?key=__`bar`__&token=__`baz`__
|
||||
|
||||
In other words the incoming webhook URL with `https` replaced by `googlechat`.
|
||||
|
||||
Google Chat was previously known as Hangouts Chat. Using `hangouts` in
|
||||
the service URL instead `googlechat` is still supported, although
|
||||
deprecated.
|
||||
|
||||
## Creating an incoming webhook in Google Chat
|
||||
|
||||
1. Open the room you would like to add Shoutrrr to and open the chat
|
||||
room menu.
|
||||

|
||||
|
||||
2. Then click on *Configure webhooks*.
|
||||

|
||||
|
||||
3. Name the webhook and save.
|
||||

|
||||
|
||||
4. Copy the URL.
|
||||

|
||||
|
||||
|
||||
5. Format the service URL by replacing `https` with `googlechat`.
|
BIN
docs/services/googlechat/hangouts-1.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
docs/services/googlechat/hangouts-2.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
docs/services/googlechat/hangouts-3.png
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
docs/services/googlechat/hangouts-4.png
Normal file
After Width: | Height: | Size: 135 KiB |
18
docs/services/gotify.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Gotify
|
||||
|
||||
## URL Format
|
||||
|
||||
--8<-- "docs/services/gotify/config.md"
|
||||
|
||||
## Examples
|
||||
|
||||
!!! example "Common usage"
|
||||
|
||||
```uri
|
||||
gotify://gotify.example.com:443/AzyoeNS.D4iJLVa/?title=Great+News&priority=1
|
||||
```
|
||||
|
||||
!!! example "With subpath"
|
||||
```uri
|
||||
gotify://example.com:443/path/to/gotify/AzyoeNS.D4iJLVa/?title=Great+News&priority=1
|
||||
```
|
7
docs/services/hangouts.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Hangouts Chat
|
||||
|
||||
Google Chat was previously known as *Hangouts Chat*. See [Google
|
||||
Chat](googlechat.md).
|
||||
|
||||
Using `hangouts` in the service URL instead `googlechat` is still
|
||||
supported, although deprecated.
|
8
docs/services/ifttt.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# IFTTT
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
ifttt://__`key`__/?events=__`event1`__[,__`event2`__,...]&value1=__`value1`__&value2=__`value2`__&value3=__`value3`__
|
||||
|
||||
--8<-- "docs/services/ifttt/config.md"
|
21
docs/services/join.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Join
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
join://shoutrrr:__`api-key`__@join/?devices=__`device1`__[,__`device2`__, ...][&icon=__`icon`__][&title=__`title`__]
|
||||
|
||||
--8<-- "docs/services/join/config.md"
|
||||
|
||||
## Guide
|
||||
|
||||
1. Go to the [Join Webapp](https://joinjoaomgcd.appspot.com/)
|
||||
2. Select your device
|
||||
3. Click **Join API**
|
||||
4. Your `deviceId` is shown in the top
|
||||
5. Click **Show** next to `API Key` to see your key
|
||||
6. Your Shoutrrr URL will then be:
|
||||
`join://shoutrrr:`__`api-key`__`@join/?devices=`__`deviceId`__
|
||||
|
||||
!!! note ""
|
||||
Multiple `deviceId`s can be combined with a `,` (repeat steps 2-4).
|
46
docs/services/lark.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Lark
|
||||
|
||||
Send notifications to Lark using a custom bot webhook.
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
lark://__`host`__/__`token`__?secret=__`secret`__&title=__`title`__&link=__`url`__
|
||||
|
||||
--8<-- "docs/services/lark/config.md"
|
||||
|
||||
- `host`: The bot API host (`open.larksuite.com` for Lark, `open.feishu.cn` for Feishu).
|
||||
- `token`: The bot webhook token (required).
|
||||
- `secret`: Optional bot secret for signed requests.
|
||||
- `title`: Optional message title (switches to post format if set).
|
||||
- `link`: Optional URL to include as a clickable link in the message.
|
||||
|
||||
### Example URL
|
||||
|
||||
```url
|
||||
lark://open.larksuite.com/abc123?secret=xyz789&title=Alert&link=https://example.com
|
||||
```
|
||||
|
||||
## Create a Custom Bot in Lark
|
||||
|
||||
Official Documentation: [Custom Bot Guide](https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot)
|
||||
|
||||
1. __Invite the Custom Bot to a Group__:
|
||||
a. Open the target group, click `More` in the upper-right corner, and then select `Settings`.
|
||||
b. In the `Settings` panel, click `Group Bot`.
|
||||
c. Click `Add a Bot` under `Group Bot`.
|
||||
d. In the `Add Bot` dialog, locate `Custom Bot` and select it.
|
||||
e. Set the bot’s name and description, then click `Add`.
|
||||
f. Copy the webhook address and click `Finish`.
|
||||
|
||||
2. __Get Host and Token__:
|
||||
- For __Lark__: Use `host = open.larksuite.com`.
|
||||
- For __Feishu__: Use `host = open.feishu.cn`.
|
||||
- The `token` is the last segment of the webhook URL.
|
||||
For example, in `https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxx`, the token is `xxxxxxxxxxxxxxxxx`.
|
||||
|
||||
3. __Get Secret (Optional)__:
|
||||
a. In group settings, open the bot list, find your custom bot, and select it to access its configuration.
|
||||
b. Under `Security Settings`, enable `Signature Verification`.
|
||||
c. Click `Copy` to save the secret.
|
||||
d. Click `Save` to apply the changes.
|
6
docs/services/logger.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Logger
|
||||
|
||||
No configuration options are available for this service.
|
||||
|
||||
It simply emits notifications to the Shoutrrr log which is
|
||||
configured by the consumer.
|
44
docs/services/matrix.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Matrix
|
||||
|
||||
!!! note Usage of the `title` parameter
|
||||
Do note that Matrix will discard any information put in the `title` parameter as the service has no analogue to a
|
||||
a title. Instead, use a custom message format to supply your wanted title as part of the message.
|
||||
|
||||
## URL Format
|
||||
|
||||
*matrix://__`user`__:__`password`__@__`host`__:__`port`__/[?rooms=__`!roomID1`__[,__`roomAlias2`__]][&disableTLS=yes]*
|
||||
|
||||
--8<-- "docs/services/matrix/config.md"
|
||||
|
||||
## Authentication
|
||||
|
||||
If no `user` is specified, the `password` is treated as the authentication token. This means that no matter what login
|
||||
flow your server uses, if you can manually retrieve a token, then Shoutrrr can use it.
|
||||
|
||||
### Password Login Flow
|
||||
|
||||
If a `user` and `password` is supplied, the `m.login.password` login flow is attempted if the server supports it.
|
||||
|
||||
## Rooms
|
||||
|
||||
If `rooms` are *not* specified, the service will send the message to all the rooms that the user has currently joined.
|
||||
|
||||
Otherwise, the service will only send the message to the specified rooms. If the user is *not* in any of those rooms,
|
||||
but have been invited to it, it will automatically accept that invite.
|
||||
|
||||
**Note**: The service will **not** join any rooms unless they are explicitly specified in `rooms`. If you need the user
|
||||
to join those rooms, you can send a notification with `rooms` explicitly set once.
|
||||
|
||||
### Room Lookup
|
||||
|
||||
Rooms specified in `rooms` will be treated as room IDs if the start with a `!` and used directly to identify rooms. If
|
||||
they have no such prefix (or use a *correctly escaped* `#`) they will instead be treated as aliases, and a directory
|
||||
lookup will be used to resolve their corresponding IDs.
|
||||
|
||||
**Note**: Don't use unescaped `#` for the channel aliases as that will be treated as the `fragment` part of the URL.
|
||||
Either omit them or URL encode them, I.E. `rooms=%23alias:server` or `rooms=alias:server`
|
||||
|
||||
### TLS
|
||||
|
||||
If you do not have TLS enabled on the server you can disable it by providing `disableTLS=yes`. This will effectively
|
||||
use `http` intead of `https` for the API calls.
|
69
docs/services/mattermost.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# MatterMost
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__][?icon=__`smiley`__&disabletls=__`yes`__]
|
||||
|
||||
--8<-- "docs/services/mattermost/config.md"
|
||||
|
||||
|
||||
|
||||
## Creating a Webhook in MatterMost
|
||||
|
||||
1. Open up the Integrations page by clicking on *Integrations* within the menu
|
||||

|
||||
|
||||
2. Click *Incoming Webhooks*
|
||||

|
||||
|
||||
3. Click *Add Incoming Webhook*
|
||||

|
||||
|
||||
4. Fill in the information for the webhook and click *Save*
|
||||

|
||||
|
||||
5. If you did everything correctly, MatterMost will give you the *URL* to your newly created webhook
|
||||

|
||||
|
||||
6. Format the service URL
|
||||
```
|
||||
https://your-domain.com/hooks/bywsw8zt5jgpte3nm65qjiru6h
|
||||
└────────────────────────┘
|
||||
token
|
||||
mattermost://your-domain.com/bywsw8zt5jgpte3nm65qjiru6h
|
||||
└────────────────────────┘
|
||||
token
|
||||
```
|
||||
|
||||
## Additional URL configuration
|
||||
|
||||
Mattermost provides functionality to post as another user or to another channel, compared to the webhook configuration.
|
||||
<br/>
|
||||
To do this, you can add a *user* and/or *channel* to the service URL.
|
||||
|
||||
```
|
||||
mattermost://shoutrrrUser@your-domain.com/bywsw8zt5jgpte3nm65qjiru6h/shoutrrrChannel
|
||||
└──────────┘ └────────────────────────┘ └─────────────┘
|
||||
user token channel
|
||||
```
|
||||
|
||||
## Passing parameters via code
|
||||
|
||||
If you want to, you also have the possibility to pass parameters to the `send` function.
|
||||
<br/>
|
||||
The following example contains all parameters that are currently supported.
|
||||
|
||||
```gotemplate
|
||||
params := (*types.Params)(
|
||||
&map[string]string{
|
||||
"username": "overwriteUserName",
|
||||
"channel": "overwriteChannel",
|
||||
"icon": "overwriteIcon",
|
||||
},
|
||||
)
|
||||
|
||||
service.Send("this is a message", params)
|
||||
```
|
||||
|
||||
This will overwrite any options, that you passed via URL.
|
BIN
docs/services/mattermost/1.PNG
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
docs/services/mattermost/2.PNG
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
docs/services/mattermost/3.PNG
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
docs/services/mattermost/4.PNG
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
docs/services/mattermost/5.PNG
Normal file
After Width: | Height: | Size: 33 KiB |
7
docs/services/ntfy.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Ntfy
|
||||
|
||||
Upstream docs: https://docs.ntfy.sh/publish/
|
||||
|
||||
## URL Format
|
||||
|
||||
--8<-- "docs/services/ntfy/config.md"
|
66
docs/services/opsgenie.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# OpsGenie
|
||||
|
||||
## URL Format
|
||||
|
||||
--8<-- "docs/services/opsgenie/config.md"
|
||||
|
||||
## Creating a REST API endpoint in OpsGenie
|
||||
|
||||
1. Open up the Integration List page by clicking on *Settings => Integration List* within the menu
|
||||

|
||||
|
||||
2. Click *API => Add*
|
||||
|
||||
3. Make sure *Create and Update Access* and *Enabled* are checked and click *Save Integration*
|
||||

|
||||
|
||||
4. Copy the *API Key*
|
||||
|
||||
5. Format the service URL
|
||||
|
||||
The host can be either api.opsgenie.com or api.eu.opsgenie.com depending on the location of your instance. See
|
||||
the [OpsGenie documentation](https://docs.opsgenie.com/docs/alert-api) for details.
|
||||
|
||||
```
|
||||
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889
|
||||
└───────────────────────────────────┘
|
||||
token
|
||||
```
|
||||
|
||||
## Passing parameters via code
|
||||
|
||||
If you want to, you can pass additional parameters to the `send` function.
|
||||
<br/>
|
||||
The following example contains all parameters that are currently supported.
|
||||
|
||||
```go
|
||||
service.Send("An example alert message", &types.Params{
|
||||
"alias": "Life is too short for no alias",
|
||||
"description": "Every alert needs a description",
|
||||
"responders": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"NOC","type":"team"}]`,
|
||||
"visibleTo": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"rocket_team","type":"team"}]`,
|
||||
"actions": "An action",
|
||||
"tags": "tag1 tag2",
|
||||
"details": `{"key1": "value1", "key2": "value2"}`,
|
||||
"entity": "An example entity",
|
||||
"source": "The source",
|
||||
"priority": "P1",
|
||||
"user": "Dracula",
|
||||
"note": "Here is a note",
|
||||
})
|
||||
```
|
||||
|
||||
## Optional parameters
|
||||
|
||||
You can optionally specify the parameters in the URL:
|
||||
|
||||
!!! info ""
|
||||
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889?alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&actions=An+action&tags=["tag1","tag2"]&entity=An+example+entity&source=The+source&priority=P1&user=Dracula¬e=Here+is+a+note
|
||||
|
||||
Example using the command line:
|
||||
|
||||
```shell
|
||||
shoutrrr send -u 'opsgenie://api.eu.opsgenie.com/token?tags=["tag1","tag2"]&description=testing&responders=[{"username":"superuser", "type": "user"}]&entity=Example Entity&source=Example Source&actions=["asdf", "bcde"]' -m "Hello World6"
|
||||
```
|
||||
|
||||
|
BIN
docs/services/opsgenie/1.png
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
docs/services/opsgenie/2.png
Normal file
After Width: | Height: | Size: 132 KiB |
32
docs/services/overview.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Services overview
|
||||
|
||||
Click on the service for a more thorough explanation. <!-- @formatter:off -->
|
||||
|
||||
| Service | URL format |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Bark](./bark.md) | *bark://__`devicekey`__@__`host`__* |
|
||||
| [Discord](./discord.md) | *discord://__`token`__@__`id`__[?thread_id=__`threadid`__]* |
|
||||
| [Email](./email.md) | *smtp://__`username`__:__`password`__@__`host`__:__`port`__/?from=__`fromAddress`__&to=__`recipient1`__[,__`recipient2`__,...]* |
|
||||
| [Gotify](./gotify.md) | *gotify://__`gotify-host`__/__`token`__* |
|
||||
| [Google Chat](./googlechat.md) | *googlechat://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz* |
|
||||
| [IFTTT](./ifttt.md) | *ifttt://__`key`__/?events=__`event1`__[,__`event2`__,...]&value1=__`value1`__&value2=__`value2`__&value3=__`value3`__* |
|
||||
| [Join](./join.md) | *join://shoutrrr:__`api-key`__@join/?devices=__`device1`__[,__`device2`__, ...][&icon=__`icon`__][&title=__`title`__]* |
|
||||
| [Mattermost](./mattermost.md) | *mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__]* |
|
||||
| [Matrix](./matrix.md) | *matrix://__`username`__:__`password`__@__`host`__:__`port`__/[?rooms=__`!roomID1`__[,__`roomAlias2`__]]* |
|
||||
| [Ntfy](./ntfy.md) | *ntfy://__`username`__:__`password`__@ntfy.sh/__`topic`__* |
|
||||
| [OpsGenie](./opsgenie.md) | *opsgenie://__`host`__/token?responders=__`responder1`__[,__`responder2`__]* |
|
||||
| [Pushbullet](./pushbullet.md) | *pushbullet://__`api-token`__[/__`device`__/#__`channel`__/__`email`__]* |
|
||||
| [Pushover](./pushover.md) | *pushover://shoutrrr:__`apiToken`__@__`userKey`__/?devices=__`device1`__[,__`device2`__, ...]* |
|
||||
| [Rocketchat](./rocketchat.md) | *rocketchat://[__`username`__@]__`rocketchat-host`__/__`token`__[/__`channel`|`@recipient`__]* |
|
||||
| [Slack](./slack.md) | *slack://[__`botname`__@]__`token-a`__/__`token-b`__/__`token-c`__* |
|
||||
| [Teams](./teams.md) | *teams://__`group`__@__`tenant`__/__`altId`__/__`groupOwner`__?host=__`organization`__.webhook.office.com* |
|
||||
| [Telegram](./telegram.md) | *telegram://__`token`__@telegram?chats=__`@channel-1`__[,__`chat-id-1`__,...]* |
|
||||
| [Zulip Chat](./zulip.md) | *zulip://__`bot-mail`__:__`bot-key`__@__`zulip-domain`__/?stream=__`name-or-id`__&topic=__`name`__* |
|
||||
| [Lark](./lark.md) | *lark://__`host`__/__`token`__?secret=__`secret`__&title=__`title`__&link=__`url`__* |
|
||||
|
||||
## Specialized services
|
||||
|
||||
| Service | Description |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Logger](./logger.md) | Writes notification to a configured go `log.Logger` |
|
||||
| [Generic Webhook](./generic.md) | Sends notifications directly to a webhook |
|
8
docs/services/pushbullet.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Pushbullet
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
pushbullet://__`api-token`__[/__`device`__/#__`channel`__/__`email`__]
|
||||
|
||||
--8<-- "docs/services/pushbullet/config.md"
|
32
docs/services/pushover.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Pushover
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
pushover://shoutrrr:__`apiToken`__@__`userKey`__/?devices=__`device1`__[,__`device2`__, ...]
|
||||
|
||||
--8<-- "docs/services/pushover/config.md"
|
||||
|
||||
## Getting the keys from Pushover
|
||||
|
||||
At your [Pushover dashboard](https://pushover.net/) you can view your __`userKey`__ in the top right.
|
||||

|
||||
|
||||
The `Name` column of the device list is what is used to refer to your devices (__`device1`__ etc.)
|
||||

|
||||
|
||||
At the bottom of the same page there are links your _applications_, where you can find your __`apiToken`__
|
||||

|
||||
|
||||
The __`apiToken`__ is displayed at the top of the application page.
|
||||

|
||||
|
||||
## Optional parameters
|
||||
|
||||
You can optionally specify the __`title`__ and __`priority`__ parameters in the URL:
|
||||
*pushover://shoutrrr:__`token`__@__`userKey`__/?devices=__`device`__&title=Custom+Title&priority=1*
|
||||
|
||||
!!! note
|
||||
Only supply priority values between -1 and 1, since 2 requires additional parameters that are not supported yet.
|
||||
|
||||
Please refer to the [Pushover API documentation](https://pushover.net/api#messages) for more information.
|
BIN
docs/services/pushover/po-1.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/services/pushover/po-2.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/services/pushover/po-3.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/services/pushover/po-4.png
Normal file
After Width: | Height: | Size: 12 KiB |
66
docs/services/rocketchat.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Rocket.chat
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
rocketchat://[__`username`__@]__`rocketchat-host`__/__`token`__[/__`channel`|`@recipient`__]*
|
||||
|
||||
--8<-- "docs/services/rocketchat/config.md"
|
||||
|
||||
## Creating a Webhook in Rocket.chat
|
||||
|
||||
1. Open up the chat Administration by clicking on *Administration* menu
|
||||

|
||||
|
||||
2. Open *Integrations* and then click *New*
|
||||

|
||||
|
||||
3. Fill in the information for the webhook and click *Save*. Please don't forget to Enable your integration.
|
||||

|
||||
|
||||
5. If you did everything correctly, Rocket.chat will give you the *URL* and *Token* to your newly created webhook.
|
||||

|
||||
|
||||
6. Format the service URL
|
||||
```
|
||||
rocketchat://your-domain.com/8eGdRzc9r4YYNyvge/2XYQcX9NBwJBKfQnphpebPcnXZcPEi32Nt4NKJfrnbhsbRfX
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
token
|
||||
```
|
||||
|
||||
## Additional URL configuration
|
||||
|
||||
Rocket.chat provides functionality to post as another user or to another channel / user, compared to the webhook configuration.
|
||||
<br/>
|
||||
To do this, you can add a *sender* and/or *channel* / *receiver* to the service URL.
|
||||
|
||||
```
|
||||
rocketchat://shoutrrrUser@your-domain.com/8eGdRzc9r4YYNyvge/2XYQcX9NBwJBKfQnphpebPcnXZcPEi32Nt4NKJfrnbhsbRfX/shoutrrrChannel
|
||||
└──────────┘ └────────────────────────────────────────────────────────────────┘ └─────────────┘
|
||||
sender token channel
|
||||
|
||||
rocketchat://shoutrrrUser@your-domain.com/8eGdRzc9r4YYNyvge/2XYQcX9NBwJBKfQnphpebPcnXZcPEi32Nt4NKJfrnbhsbRfX/@shoutrrrReceiver
|
||||
└──────────┘ └────────────────────────────────────────────────────────────────┘ └───────────────┘
|
||||
sender token receiver
|
||||
```
|
||||
|
||||
## Passing parameters via code
|
||||
|
||||
If you want to, you also have the possibility to pass parameters to the `send` function.
|
||||
<br/>
|
||||
The following example contains all parameters that are currently supported.
|
||||
|
||||
```gotemplate
|
||||
params := (*types.Params)(
|
||||
&map[string]string{
|
||||
"username": "overwriteUserName",
|
||||
"channel": "overwriteChannel",
|
||||
},
|
||||
)
|
||||
|
||||
service.Send("this is a message", params)
|
||||
```
|
||||
|
||||
This will overwrite any options, that you passed via URL.
|
||||
|
||||
For more Rocket.chat Webhooks options see [official guide](https://docs.rocket.chat/guides/administrator-guides/integrations).
|
BIN
docs/services/rocketchat/1.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
docs/services/rocketchat/2.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
docs/services/rocketchat/3.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
docs/services/rocketchat/4.png
Normal file
After Width: | Height: | Size: 126 KiB |
36
docs/services/slack.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Slack
|
||||
|
||||
!!! attention "New URL format"
|
||||
The URL format for Slack has been changed to allow for API- as well as webhook tokens.
|
||||
Using the old format (`slack://xxxx/yyyy/zzzz`) will still work as before and will automatically be upgraded to
|
||||
the new format when used.
|
||||
|
||||
The Slack notification service uses either [Slack Webhooks](https://api.slack.com/messaging/webhooks) or the
|
||||
[Bot API](https://api.slack.com/methods/chat.postMessage) to send messages.
|
||||
|
||||
See the [guides](../guides/slack/index.md) for information on how to get your *token* and *channel*.
|
||||
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! note ""
|
||||
Note that the token uses a prefix to determine the type, usually either `hook` (for webhooks) or `xoxb` (for bot API).
|
||||
|
||||
--8<-- "docs/services/slack/config.md"
|
||||
|
||||
!!! info "Color format"
|
||||
The format for the `Color` prop follows the [slack docs](https://api.slack.com/reference/messaging/attachments#fields)
|
||||
but `#` needs to be escaped as `%23` when passed in a URL.
|
||||
So <span style="background:#ff8000;width:.9em;height:.9em;display:inline-block;vertical-align:middle"></span><code>#ff8000</code> would be `%23ff8000` etc.
|
||||
|
||||
## Examples
|
||||
|
||||
!!! example "Bot API"
|
||||
```uri
|
||||
slack://xoxb:123456789012-1234567890123-4mt0t4l1YL3g1T5L4cK70k3N@C001CH4NN3L?color=good&title=Great+News&icon=man-scientist&botname=Shoutrrrbot
|
||||
```
|
||||
|
||||
!!! example "Webhook"
|
||||
```uri
|
||||
slack://hook:WNA3PBYV6-F20DUQND3RQ-Webc4MAvoacrpPakR8phF0zi@webhook?color=good&title=Great+News&icon=man-scientist&botname=Shoutrrrbot
|
||||
```
|
69
docs/services/teams.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Teams
|
||||
|
||||
!!! attention "New webhook URL format only"
|
||||
Shoutrrr now only supports the new Teams webhook URL format with an
|
||||
organization-specific domain.
|
||||
|
||||
You must specify your organization domain using:
|
||||
|
||||
```text
|
||||
?host=example.webhook.office.com
|
||||
```
|
||||
Where `example` is your organization's short name.
|
||||
|
||||
Legacy webhook formats (e.g., `outlook.office.com`) are no longer supported.
|
||||
|
||||
## URL Format
|
||||
|
||||
```
|
||||
teams://group@tenant/altId/groupOwner/extraId?host=organization.webhook.office.com[&color=color][&title=title]
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `group`: The first UUID component from the webhook URL.
|
||||
- `tenant`: The second UUID component from the webhook URL.
|
||||
- `altId`: The third component (hex string) from the webhook URL.
|
||||
- `groupOwner`: The fourth UUID component from the webhook URL.
|
||||
- `extraId`: The fifth component at the end of the webhook URL.
|
||||
- `organization`: Your organization name for the webhook domain (required).
|
||||
- `color`: Optional hex color code for the message card (e.g., `FF0000` for red).
|
||||
- `title`: Optional title for the message card.
|
||||
|
||||
--8<-- "docs/services/teams/config.md"
|
||||
|
||||
## Setting up a webhook
|
||||
|
||||
To use the Microsoft Teams notification service, you need to set up a custom
|
||||
incoming webhook. Follow the instructions in [this Microsoft guide](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#create-an-incoming-webhook).
|
||||
|
||||
## Extracting the token
|
||||
|
||||
The token is extracted from your webhook URL:
|
||||
|
||||
<pre><code>https://<b><organization></b>.webhook.office.com/webhookb2/<b><group></b>@<b><tenant></b>/IncomingWebhook/<b><altId></b>/<b><groupOwner></b>/<b><extraId></b></code></pre>
|
||||
|
||||
!!! note "Important components"
|
||||
All parts of the webhook URL are required:
|
||||
|
||||
- `organization`: Your organization name (e.g., `contoso`).
|
||||
- `group`: First UUID component.
|
||||
- `tenant`: Second UUID component.
|
||||
- `altId`: Third component (hex string).
|
||||
- `groupOwner`: Fourth UUID component.
|
||||
- `extraId`: Fifth component.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
# Original webhook URL:
|
||||
https://contoso.webhook.office.com/webhookb2/11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/IncomingWebhook/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc/V2ESyij_gAljSoUQHvZoZYzlpAoAXExyOl26dlf1xHEx05
|
||||
|
||||
# Shoutrrr URL:
|
||||
teams://11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc/V2ESyij_gAljSoUQHvZoZYzlpAoAXExyOl26dlf1xHEx05?host=contoso.webhook.office.com&color=FF0000&title=Alert
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `color=FF0000` sets a red theme.
|
||||
- `title=Alert` adds a custom title to the message card.
|
73
docs/services/telegram.md
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Telegram
|
||||
|
||||
## URL Format
|
||||
|
||||
!!! info ""
|
||||
telegram://__`token`__@telegram?chats=__`channel-1`__[,__`chat-id-1`__,...]
|
||||
|
||||
--8<-- "docs/services/telegram/config.md"
|
||||
|
||||
## Getting a token for Telegram
|
||||
|
||||
Talk to [the botfather](https://core.telegram.org/bots#6-botfather).
|
||||
|
||||
## Identifying the target chats/channels
|
||||
|
||||
The `chats` param consists of one or more `Chat ID`s or `channel name`s.
|
||||
|
||||
### Public Channels
|
||||
The channel names can be retrieved in the telegram client in the `Channel info` section for public channels.
|
||||
Replace the `t.me/` prefix from the link with a `@`.
|
||||
|
||||
!!! note
|
||||
Channels names need to be prefixed by `@` to identify them as such.
|
||||
|
||||
!!! note
|
||||
If your channel only has an invite link (starting with `t.me/+`), you have to use it's Chat ID (see below)
|
||||
|
||||
!!! note
|
||||
A `message_thread_id` param ([reference](https://core.telegram.org/bots/api#sendmessage)) can be added, with the format of `$chat_id:$message_thread_id`. [More info](https://stackoverflow.com/questions/74773675/how-to-get-topic-id-for-telegram-group-chat/75178418#75178418) on how to obtain the `message_thread_id`.
|
||||
|
||||
### Chats
|
||||
Private channels, Group chats and private chats are identified by `Chat ID`s. Unfortunatly, they are generally not visible in the
|
||||
telegram clients.
|
||||
The easiest way to retrieve them is by using the `shoutrrr generate telegram` command which will guide you through
|
||||
creating a URL with your target chats.
|
||||
|
||||
!!! tip
|
||||
You can use the `nickfedor/shoutrrr` image in docker to run it without download/installing the `shoutrrr` CLI using:
|
||||
```
|
||||
docker run --rm -it nickfedor/shoutrrr generate telegram
|
||||
```
|
||||
|
||||
### Asking @shoutrrrbot
|
||||
Another way of retrieving the Chat IDs, is by forwarding a message from the target chat to the [@shoutrrrbot](https://t.me/shoutrrrbot).
|
||||
It will reply with the Chat ID for the chat where the forwarded message was originally posted.
|
||||
Note that it will not work correctly for Group chats, as those messages are just seen as being posted by a user, not in a specific chat.
|
||||
Instead you can use the second method, which is to invite the @shoutrrrbot into your group chat and address a message to it (start the message with @shoutrrrbot). You can then safely kick the bot from the group.
|
||||
|
||||
The bot should be constantly online, unless it's usage exceeds the free tier on GCP. It's source is available at [github.com/nicholas-fedor/shoutrrrbot](https://github.com/nicholas-fedor/shoutrrrbot).
|
||||
|
||||
|
||||
|
||||
## Optional parameters
|
||||
|
||||
You can optionally specify the __`notification`__, __`parseMode`__ and __`preview`__ parameters in the URL:
|
||||
|
||||
!!! info ""
|
||||
<pre>telegram://__`token`__@__`telegram`__/?channels=__`channel`__¬ification=no&preview=false&parseMode=html</pre>
|
||||
|
||||
See [the telegram documentation](https://core.telegram.org/bots/api#sendmessage) for more information.
|
||||
|
||||
!!! note
|
||||
`preview` and `notification` are inverted in regards to their API counterparts (`disable_web_page_preview` and `disable_notification`)
|
||||
|
||||
### Parse Mode and Title
|
||||
|
||||
If a parse mode is specified, the message needs to be escaped as per the corresponding sections in
|
||||
[Formatting options](https://core.telegram.org/bots/api#formatting-options).
|
||||
|
||||
When a title has been specified, it will be prepended to the message, but this is only supported for
|
||||
the `HTML` parse mode. Note that, if no parse mode is specified, the message will be escaped and sent using `HTML`.
|
||||
|
||||
Since the markdown modes are really hard to escape correctly, it's recommended to stick to `HTML` parse mode.
|
29
docs/services/zulip.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Zulip Chat
|
||||
|
||||
## URL Format
|
||||
|
||||
The shoutrrr service URL should look like this:
|
||||
!!! info ""
|
||||
zulip://__`botmail`__:__`botkey`__@__`host`__/?stream=__`stream`__&topic=__`topic`__
|
||||
|
||||
--8<-- "docs/services/zulip/config.md"
|
||||
|
||||
!!! note
|
||||
Since __`botmail`__ is a mail address you need to URL escape the `@` in it to `%40`.
|
||||
|
||||
### Examples
|
||||
|
||||
Stream and topic are both optional and can be given as parameters to the Send method:
|
||||
|
||||
```go
|
||||
sender, _ := shoutrrr.CreateSender(url)
|
||||
|
||||
params := make(types.Params)
|
||||
params["stream"] = "mystream"
|
||||
params["topic"] = "This is my topic"
|
||||
|
||||
sender.Send(message, ¶ms)
|
||||
```
|
||||
|
||||
!!! example "Example service URL"
|
||||
zulip://my-bot%40zulipchat.com:correcthorsebatterystable@example.zulipchat.com?stream=foo&topic=bar
|
BIN
docs/shoutrrr-180px.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/shoutrrr-logotype.png
Normal file
After Width: | Height: | Size: 132 KiB |
269
docs/shoutrrr-logotype.svg
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
docs/shoutrrr.jpg
Normal file
After Width: | Height: | Size: 53 KiB |
30
docs/stylesheets/extra.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
.md-typeset li img {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.md-typeset figure {
|
||||
background: var(--md-code-bg-color);
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-typeset figure img {
|
||||
box-shadow: 2px 2px 4px #00000080;
|
||||
padding: 3px;
|
||||
background: var(--md-code-bg-color);
|
||||
}
|
||||
|
||||
|
||||
.md-typeset li img:last-child {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.badges img {
|
||||
height: 20px;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
8
docs/stylesheets/theme.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
[data-md-color-scheme="shoutrrr"] {
|
||||
--md-primary-fg-color: hsl(193, 100%, 44%);
|
||||
--md-primary-fg-color--light: hsl(193, 100%, 55%);
|
||||
--md-primary-fg-color--dark: hsl(193, 76%, 33%);
|
||||
--md-accent-fg-color: hsl(39, 84%, 44%);
|
||||
--md-accent-fg-color--transparent: hsla(39, 84%, 58%, 0.1);
|
||||
--md-typeset-a-color: var(--md-primary-fg-color--dark);
|
||||
}
|
31
generate-release-notes.sh
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
current=$1
|
||||
if [ -z "$current" ]; then
|
||||
echo "Missing argument VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tags=($(git tag --list))
|
||||
|
||||
for i in "${!tags[@]}"; do
|
||||
if [[ "${tags[$i]}" = "$current" ]]; then
|
||||
previous="${tags[$i - 1]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$previous" ]; then
|
||||
echo "Invalid tag, or could not find previous tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\e[97mListing changes from \e[96m$previous\e[97m to \e[96m$current\e[0m:\n"
|
||||
|
||||
changes=$(git log --pretty=format:"* %h %s" $previous...$current)
|
||||
|
||||
echo "## Changelog"
|
||||
echo "$changes" | grep -v "chore(deps)" | grep -v "Merge " | grep -v "chore(ci)"
|
||||
echo
|
||||
echo "### Dependencies"
|
||||
echo "$changes" | grep "chore(deps)"
|
27
generate-service-config-docs.sh
Executable file
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
function generate_docs() {
|
||||
SERVICE=$1
|
||||
DOCSPATH=./docs/services/$SERVICE
|
||||
echo -en "Creating docs for \e[96m$SERVICE\e[0m... "
|
||||
mkdir -p "$DOCSPATH"
|
||||
go run ./shoutrrr docs -f markdown "$SERVICE" > "$DOCSPATH"/config.md
|
||||
if [ $? ]; then
|
||||
echo -e "Done!"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
generate_docs "$1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for S in ./pkg/services/*; do
|
||||
SERVICE=$(basename "$S")
|
||||
if [[ "$SERVICE" == "standard" ]] || [[ "$SERVICE" == "xmpp" ]] || [[ -f "$S" ]]; then
|
||||
continue
|
||||
fi
|
||||
generate_docs "$SERVICE"
|
||||
done
|
44
go.mod
Normal file
|
@ -0,0 +1,44 @@
|
|||
module github.com/nicholas-fedor/shoutrrr
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/jarcoal/httpmock v1.4.0
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/onsi/ginkgo/v2 v2.23.4
|
||||
github.com/onsi/gomega v1.37.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
96
go.sum
Normal file
|
@ -0,0 +1,96 @@
|
|||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
|
||||
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
110
goreleaser.yml
Normal file
|
@ -0,0 +1,110 @@
|
|||
version: 2
|
||||
|
||||
builds:
|
||||
- main: ./shoutrrr/main.go
|
||||
binary: shoutrrr/shoutrrr
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/nicholas-fedor/shoutrrr/internal/meta.Version={{ .Version }}
|
||||
|
||||
archives:
|
||||
- id: default # Unique ID for this archive configuration
|
||||
name_template: >-
|
||||
{{- .ProjectName }}_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}amd64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else if eq .Arch "arm" }}armhf
|
||||
{{- else if eq .Arch "arm64" }}arm64v8
|
||||
{{- else }}{{ .Arch }}{{ end }}_
|
||||
{{- .Version -}}
|
||||
files:
|
||||
- LICENSE.md
|
||||
builds:
|
||||
- shoutrrr
|
||||
formats: ["tar.gz"]
|
||||
- id: windows
|
||||
name_template: >-
|
||||
{{- .ProjectName }}_
|
||||
{{- .Os }}_
|
||||
{{- if eq .Arch "amd64" }}amd64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else if eq .Arch "arm" }}armhf
|
||||
{{- else if eq .Arch "arm64" }}arm64v8
|
||||
{{- else }}{{ .Arch }}{{ end }}_
|
||||
{{- .Version -}}
|
||||
files:
|
||||
- LICENSE.md
|
||||
builds:
|
||||
- shoutrrr
|
||||
formats: ["zip"]
|
||||
|
||||
dockers:
|
||||
- use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--label=org.opencontainers.image.created={{ .Date }}"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ""
|
||||
dockerfile: dockerfiles/Dockerfile
|
||||
image_templates:
|
||||
- nickfedor/shoutrrr:amd64-{{ .Version }}
|
||||
- nickfedor/shoutrrr:amd64-latest
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:amd64-{{ .Version }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:amd64-latest
|
||||
- use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/i386"
|
||||
- "--label=org.opencontainers.image.created={{ .Date }}"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
|
||||
goos: linux
|
||||
goarch: "386"
|
||||
goarm: ""
|
||||
dockerfile: dockerfiles/Dockerfile
|
||||
image_templates:
|
||||
- nickfedor/shoutrrr:i386-{{ .Version }}
|
||||
- nickfedor/shoutrrr:i386-{{ if .IsSnapshot }}latest-dev{{ else }}latest{{ end }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:i386-{{ .Version }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:i386-{{ if .IsSnapshot }}latest-dev{{ else }}latest{{ end }}
|
||||
- use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm/v6"
|
||||
- "--label=org.opencontainers.image.created={{ .Date }}"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
dockerfile: dockerfiles/Dockerfile
|
||||
image_templates:
|
||||
- nickfedor/shoutrrr:armhf-{{ .Version }}
|
||||
- nickfedor/shoutrrr:armhf-{{ if .IsSnapshot }}latest-dev{{ else }}latest{{ end }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:armhf-{{ .Version }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:armhf-{{ if .IsSnapshot }}latest-dev{{ else }}latest{{ end }}
|
||||
- use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--label=org.opencontainers.image.created={{ .Date }}"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
goarm: ""
|
||||
dockerfile: dockerfiles/Dockerfile
|
||||
image_templates:
|
||||
- nickfedor/shoutrrr:arm64v8-{{ .Version }}
|
||||
- nickfedor/shoutrrr:arm64v8-latest
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:arm64v8-{{ .Version }}
|
||||
- ghcr.io/nicholas-fedor/shoutrrr:arm64v8-{{ if .IsSnapshot }}latest-dev{{ else }}latest{{ end }}
|
16
internal/dedupe/dedupe.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package dedupe
|
||||
|
||||
import "slices"
|
||||
|
||||
// RemoveDuplicates from a slice of strings.
|
||||
func RemoveDuplicates(src []string) []string {
|
||||
unique := make([]string, 0, len(src))
|
||||
for _, s := range src {
|
||||
found := slices.Contains(unique, s)
|
||||
if !found {
|
||||
unique = append(unique, s)
|
||||
}
|
||||
}
|
||||
|
||||
return unique
|
||||
}
|
41
internal/dedupe/dedupe_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package dedupe_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/dedupe"
|
||||
)
|
||||
|
||||
func TestRemoveDuplicates(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
"no duplicates": {
|
||||
input: []string{"a", "b", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate inside slice": {
|
||||
input: []string{"a", "b", "a", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate at end of slice": {
|
||||
input: []string{"a", "b", "c", "a"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate next to each other inside slice": {
|
||||
input: []string{"a", "b", "b", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := dedupe.RemoveDuplicates(tc.input)
|
||||
if !reflect.DeepEqual(tc.want, got) {
|
||||
t.Fatalf("expected: %#v, got: %#v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
68
internal/failures/failure.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package failures
|
||||
|
||||
import "fmt"
|
||||
|
||||
// FailureID is a unique identifier for a specific error type.
|
||||
type FailureID int
|
||||
|
||||
// failure is the concrete implementation of the Failure interface.
|
||||
// It wraps an error with a message and an ID for categorization.
|
||||
type failure struct {
|
||||
message string // Descriptive message for the error
|
||||
id FailureID // Unique identifier for this error type
|
||||
wrapped error // Underlying error, if any, for chaining
|
||||
}
|
||||
|
||||
// Failure extends the error interface with an ID and methods for unwrapping and comparison.
|
||||
// It allows errors to be identified by a unique ID and supports Go’s error wrapping conventions.
|
||||
type Failure interface {
|
||||
error
|
||||
ID() FailureID // Returns the unique identifier for this failure
|
||||
Unwrap() error // Returns the wrapped error, if any
|
||||
Is(target error) bool // Checks if the target error matches this failure by ID
|
||||
}
|
||||
|
||||
// Error returns the failure’s message, appending the wrapped error’s message if present.
|
||||
func (f *failure) Error() string {
|
||||
if f.wrapped == nil {
|
||||
return f.message
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %v", f.message, f.wrapped)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error wrapped by this failure, or nil if none exists.
|
||||
func (f *failure) Unwrap() error {
|
||||
return f.wrapped
|
||||
}
|
||||
|
||||
// ID returns the unique identifier assigned to this failure.
|
||||
func (f *failure) ID() FailureID {
|
||||
return f.id
|
||||
}
|
||||
|
||||
// Is reports whether the target error is a failure with the same ID.
|
||||
// It only returns true for failures of the same type with matching IDs.
|
||||
func (f *failure) Is(target error) bool {
|
||||
targetFailure, ok := target.(*failure)
|
||||
|
||||
return ok && targetFailure.id == f.id
|
||||
}
|
||||
|
||||
// Wrap creates a new failure with the given message, ID, and optional wrapped error.
|
||||
// If variadic arguments are provided, they are used to format the message using fmt.Sprintf.
|
||||
// This supports Go’s error wrapping pattern while adding a unique ID for identification.
|
||||
func Wrap(message string, failureID FailureID, wrappedError error, v ...any) Failure {
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(message, v...)
|
||||
}
|
||||
|
||||
return &failure{
|
||||
message: message,
|
||||
id: failureID,
|
||||
wrapped: wrappedError,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure failure implements the error interface at compile time.
|
||||
var _ error = &failure{}
|
174
internal/failures/failure_test.go
Normal file
|
@ -0,0 +1,174 @@
|
|||
package failures_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/format"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/failures"
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/testutils"
|
||||
)
|
||||
|
||||
// TestFailures runs the Ginkgo test suite for the failures package.
|
||||
func TestFailures(t *testing.T) {
|
||||
format.CharactersAroundMismatchToInclude = 20 // Show more context in failure output
|
||||
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
ginkgo.RunSpecs(t, "Failure Suite")
|
||||
}
|
||||
|
||||
var _ = ginkgo.Describe("the failure package", func() {
|
||||
// Common test fixtures
|
||||
var (
|
||||
testID failures.FailureID = 42 // Consistent ID for testing
|
||||
testMessage = "test failure occurred" // Sample error message
|
||||
wrappedErr = errors.New("underlying error") // Sample wrapped error
|
||||
)
|
||||
|
||||
ginkgo.Describe("Wrap function", func() {
|
||||
ginkgo.When("creating a basic failure", func() {
|
||||
ginkgo.It("returns a failure with the provided message and ID", func() {
|
||||
failure := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(testMessage))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("wrapping an existing error", func() {
|
||||
ginkgo.It("combines the message and wrapped error", func() {
|
||||
failure := failures.Wrap(testMessage, testID, wrappedErr)
|
||||
expectedError := fmt.Sprintf("%s: %v", testMessage, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(expectedError))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("using formatted message with arguments", func() {
|
||||
ginkgo.It("formats the message correctly", func() {
|
||||
formatMessage := "test failure %d"
|
||||
failure := failures.Wrap(formatMessage, testID, nil, 123)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal("test failure 123"))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Failure interface methods", func() {
|
||||
var failure failures.Failure
|
||||
|
||||
// Setup a failure with a wrapped error before each test
|
||||
ginkgo.BeforeEach(func() {
|
||||
failure = failures.Wrap(testMessage, testID, wrappedErr)
|
||||
})
|
||||
|
||||
ginkgo.Describe("Error method", func() {
|
||||
ginkgo.It("returns only the message when no wrapped error exists", func() {
|
||||
failureNoWrap := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failureNoWrap.Error()).To(gomega.Equal(testMessage))
|
||||
})
|
||||
ginkgo.It("combines message with wrapped error", func() {
|
||||
expected := fmt.Sprintf("%s: %v", testMessage, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("ID method", func() {
|
||||
ginkgo.It("returns the assigned ID", func() {
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Unwrap method", func() {
|
||||
ginkgo.It("returns the wrapped error", func() {
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
ginkgo.It("returns nil when no wrapped error exists", func() {
|
||||
failureNoWrap := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failureNoWrap.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Is method", func() {
|
||||
ginkgo.It("returns true for failures with the same ID", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
f2 := failures.Wrap("second", testID, nil)
|
||||
gomega.Expect(f1.Is(f2)).To(gomega.BeTrue())
|
||||
gomega.Expect(f2.Is(f1)).To(gomega.BeTrue())
|
||||
})
|
||||
ginkgo.It("returns false for failures with different IDs", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
f2 := failures.Wrap("second", testID+1, nil)
|
||||
gomega.Expect(f1.Is(f2)).To(gomega.BeFalse())
|
||||
gomega.Expect(f2.Is(f1)).To(gomega.BeFalse())
|
||||
})
|
||||
ginkgo.It("returns false when comparing with a non-failure error", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
gomega.Expect(f1.Is(wrappedErr)).To(gomega.BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("edge cases", func() {
|
||||
ginkgo.When("wrapping with an empty message", func() {
|
||||
ginkgo.It("handles an empty message gracefully", func() {
|
||||
failure := failures.Wrap("", testID, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(": " + wrappedErr.Error()))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("wrapping with nil error and no args", func() {
|
||||
ginkgo.It("returns a valid failure with just message and ID", func() {
|
||||
failure := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(testMessage))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("using multiple wrapped failures", func() {
|
||||
ginkgo.It("correctly chains and unwraps multiple errors", func() {
|
||||
innerErr := errors.New("inner error")
|
||||
middleErr := failures.Wrap("middle", testID+1, innerErr)
|
||||
outerErr := failures.Wrap("outer", testID, middleErr)
|
||||
gomega.Expect(outerErr.Error()).To(gomega.Equal("outer: middle: inner error"))
|
||||
gomega.Expect(outerErr.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(outerErr.Unwrap()).To(gomega.Equal(middleErr))
|
||||
gomega.Expect(middleErr.Unwrap()).To(gomega.Equal(innerErr))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("integration-like scenarios", func() {
|
||||
ginkgo.It("works with standard error wrapping utilities", func() {
|
||||
innerErr := errors.New("inner error")
|
||||
failure := failures.Wrap("wrapped failure", testID, innerErr)
|
||||
gomega.Expect(errors.Is(failure, innerErr)).To(gomega.BeTrue()) // Matches wrapped error
|
||||
gomega.Expect(errors.Unwrap(failure)).To(gomega.Equal(innerErr))
|
||||
})
|
||||
|
||||
ginkgo.It("handles fmt.Errorf wrapping", func() {
|
||||
failure := failures.Wrap("failure", testID, nil)
|
||||
wrapped := fmt.Errorf("additional context: %w", failure)
|
||||
gomega.Expect(wrapped.Error()).To(gomega.Equal("additional context: failure"))
|
||||
gomega.Expect(errors.Unwrap(wrapped)).To(gomega.Equal(failure))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("testutils integration", func() {
|
||||
ginkgo.It("can use TestLogger for logging failures", func() {
|
||||
// Demonstrate compatibility with testutils logger
|
||||
failure := failures.Wrap("logged failure", testID, nil)
|
||||
logger := testutils.TestLogger()
|
||||
logger.Printf("Error occurred: %v", failure)
|
||||
// No assertion needed; ensures no panic during logging
|
||||
})
|
||||
})
|
||||
})
|
7
internal/meta/version.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package meta
|
||||
|
||||
// Version of Shoutrrr.
|
||||
const Version = `0.6-dev`
|
||||
|
||||
// DocsVersion is prepended to documentation URLs and usually equals MAJOR.MINOR of Version.
|
||||
const DocsVersion = `dev`
|
48
internal/testutils/config.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// TestConfigGetInvalidQueryValue tests whether the config returns
|
||||
// an error when an invalid query value is requested.
|
||||
func TestConfigGetInvalidQueryValue(config types.ServiceConfig) {
|
||||
value, err := format.GetConfigQueryResolver(config).Get("invalid query var")
|
||||
gomega.ExpectWithOffset(1, value).To(gomega.BeEmpty())
|
||||
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
// TestConfigSetInvalidQueryValue tests whether the config returns
|
||||
// an error when a URL with an invalid query value is parsed.
|
||||
func TestConfigSetInvalidQueryValue(config types.ServiceConfig, rawInvalidURL string) {
|
||||
invalidURL, err := url.Parse(rawInvalidURL)
|
||||
gomega.ExpectWithOffset(1, err).
|
||||
ToNot(gomega.HaveOccurred(), "the test URL did not parse correctly")
|
||||
|
||||
err = config.SetURL(invalidURL)
|
||||
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
// TestConfigSetDefaultValues tests whether setting the default values
|
||||
// can be set for an empty config without any errors.
|
||||
func TestConfigSetDefaultValues(config types.ServiceConfig) {
|
||||
pkr := format.NewPropKeyResolver(config)
|
||||
gomega.ExpectWithOffset(1, pkr.SetDefaultProps(config)).To(gomega.Succeed())
|
||||
}
|
||||
|
||||
// TestConfigGetEnumsCount tests whether the config.Enums returns the expected amount of items.
|
||||
func TestConfigGetEnumsCount(config types.ServiceConfig, expectedCount int) {
|
||||
enums := config.Enums()
|
||||
gomega.ExpectWithOffset(1, enums).To(gomega.HaveLen(expectedCount))
|
||||
}
|
||||
|
||||
// TestConfigGetFieldsCount tests whether the config.QueryFields return the expected amount of fields.
|
||||
func TestConfigGetFieldsCount(config types.ServiceConfig, expectedCount int) {
|
||||
fields := format.GetConfigQueryResolver(config).QueryFields()
|
||||
gomega.ExpectWithOffset(1, fields).To(gomega.HaveLen(expectedCount))
|
||||
}
|
7
internal/testutils/eavesdropper.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package testutils
|
||||
|
||||
// Eavesdropper is an interface that provides a way to get a summarized output of a connection RX and TX.
|
||||
type Eavesdropper interface {
|
||||
GetConversation(includeGreeting bool) string
|
||||
GetClientSentences() []string
|
||||
}
|
36
internal/testutils/failwriter.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrWriteLimitReached = errors.New("reached write limit")
|
||||
|
||||
type failWriter struct {
|
||||
writeLimit int
|
||||
writeCount int
|
||||
}
|
||||
|
||||
// Close is just a dummy function to implement io.Closer.
|
||||
func (fw *failWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write returns an error if the write limit has been reached.
|
||||
func (fw *failWriter) Write(data []byte) (int, error) {
|
||||
fw.writeCount++
|
||||
if fw.writeCount > fw.writeLimit {
|
||||
return 0, fmt.Errorf("%w: %d", ErrWriteLimitReached, fw.writeLimit)
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// CreateFailWriter returns a io.WriteCloser that returns an error after the amount of writes indicated by writeLimit.
|
||||
func CreateFailWriter(writeLimit int) io.WriteCloser {
|
||||
return &failWriter{
|
||||
writeLimit: writeLimit,
|
||||
}
|
||||
}
|