1
0
Fork 0

Adding upstream version 0.8.9.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 10:16:14 +02:00
parent 3b2c48b5e4
commit c0c4addb85
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
285 changed files with 25880 additions and 0 deletions

173
.all-contributorsrc Normal file
View 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
View 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
View file

@ -0,0 +1,6 @@
---
engines:
coverage:
exclude_paths:
- "*.md"
- "**/*.md"

16
.github/pull_request_template.md vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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>.
![github actions workflow status](https://github.com/nicholas-fedor/shoutrrr/workflows/Main%20Workflow/badge.svg)
[![codecov](https://codecov.io/gh/nicholas-fedor/shoutrrr/branch/main/graph/badge.svg)](https://codecov.io/gh/nicholas-fedor/shoutrrr)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/47eed72de79448e2a6e297d770355544)](https://www.codacy.com/gh/nicholas-fedor/shoutrrr/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=nicholas-fedor/shoutrrr&amp;utm_campaign=Badge_Grade)
[![report card](https://goreportcard.com/badge/github.com/nicholas-fedor/shoutrrr)](https://goreportcard.com/badge/github.com/nicholas-fedor/shoutrrr)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/nicholas-fedor/shoutrrr)
[![github code size in bytes](https://img.shields.io/github/languages/code-size/nicholas-fedor/shoutrrr.svg?style=flat-square)](https://github.com/nicholas-fedor/shoutrrr)
[![license](https://img.shields.io/github/license/nicholas-fedor/shoutrrr.svg?style=flat-square)](https://github.com/nicholas-fedor/shoutrrr/blob/main/LICENSE)
[![Pulls from DockerHub](https://img.shields.io/docker/pulls/nickfedor/shoutrrr.svg)](https://hub.docker.com/r/nickfedor/shoutrrr)
[![godoc](https://godoc.org/github.com/nicholas-fedor/shoutrrr?status.svg)](https://godoc.org/github.com/nicholas-fedor/shoutrrr) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#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
View 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
View file

@ -0,0 +1,3 @@
#!/bin/sh
go build -o shoutrrr/ ./shoutrrr

12
dockerfiles/Dockerfile Normal file
View 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
View file

@ -0,0 +1,4 @@
mkdocs
mkdocs-material
mkdocs-git-revision-date-localized-plugin
mkdocs-minify-plugin

19
docs/examples/generic.md Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

20
docs/generators/basic.md Normal file
View 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&notification=Yes&parsemode=None&preview=No
```

View 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
View 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 }}.
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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
View 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&amp;utm_medium=referral&amp;utm_content=nicholas-fedor/shoutrrr&amp;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&amp;logoColor=white&amp;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
View 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
View 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
View 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
View 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.
![Screenshot 1](discord/sc-1.png)
2. In the menu on the left, click on *Integrations*.
![Screenshot 2](discord/sc-2.png)
3. In the menu on the right, click on *Create Webhook*.
![Screenshot 3](discord/sc-3.png)
4. Set the name, channel, and icon to your liking and click the *Copy Webhook URL* button.
![Screenshot 4](discord/sc-4.png)
5. Press the *Save Changes* button.
![Screenshot 5](discord/sc-5.png)
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
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

8
docs/services/email.md Normal file
View 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
View 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"

View 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.
![Screenshot 1](googlechat/hangouts-1.png)
2. Then click on *Configure webhooks*.
![Screenshot 2](googlechat/hangouts-2.png)
3. Name the webhook and save.
![Screenshot 3](googlechat/hangouts-3.png)
4. Copy the URL.
![Screenshot 4](googlechat/hangouts-4.png)
5. Format the service URL by replacing `https` with `googlechat`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

18
docs/services/gotify.md Normal file
View 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
```

View 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
View 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
View 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
View 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 bots 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
View 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
View 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.

View 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
![Screenshot 1](mattermost/1.PNG)
2. Click *Incoming Webhooks*
![Screenshot 2](mattermost/2.PNG)
3. Click *Add Incoming Webhook*
![Screenshot 3](mattermost/3.PNG)
4. Fill in the information for the webhook and click *Save*
![Screenshot 4](mattermost/4.PNG)
5. If you did everything correctly, MatterMost will give you the *URL* to your newly created webhook
![Screenshot 5](mattermost/5.PNG)
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

7
docs/services/ntfy.md Normal file
View 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
View 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
![Screenshot 1](opsgenie/1.png)
2. Click *API => Add*
3. Make sure *Create and Update Access* and *Enabled* are checked and click *Save Integration*
![Screenshot 2](opsgenie/2.png)
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&note=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"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

32
docs/services/overview.md Normal file
View 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`&#124;`@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 |

View 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
View 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.
![Screenshot 1](pushover/po-1.png)
The `Name` column of the device list is what is used to refer to your devices (__`device1`__ etc.)
![Screenshot 4](pushover/po-4.png)
At the bottom of the same page there are links your _applications_, where you can find your __`apiToken`__
![Screenshot 2](pushover/po-2.png)
The __`apiToken`__ is displayed at the top of the application page.
![Screenshot 3](pushover/po-3.png)
## 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,66 @@
# Rocket.chat
## URL Format
!!! info ""
rocketchat://[__`username`__@]__`rocketchat-host`__/__`token`__[/__`channel`&#124;`@recipient`__]*
--8<-- "docs/services/rocketchat/config.md"
## Creating a Webhook in Rocket.chat
1. Open up the chat Administration by clicking on *Administration* menu
![Screenshot 1](rocketchat/1.png)
2. Open *Integrations* and then click *New*
![Screenshot 2](rocketchat/2.png)
3. Fill in the information for the webhook and click *Save*. Please don't forget to Enable your integration.
![Screenshot 3](rocketchat/3.png)
5. If you did everything correctly, Rocket.chat will give you the *URL* and *Token* to your newly created webhook.
![Screenshot 4](rocketchat/4.png)
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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

36
docs/services/slack.md Normal file
View 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
View 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>&lt;organization&gt;</b>.webhook.office.com/webhookb2/<b>&lt;group&gt;</b>@<b>&lt;tenant&gt;</b>/IncomingWebhook/<b>&lt;altId&gt;</b>/<b>&lt;groupOwner&gt;</b>/<b>&lt;extraId&gt;</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
View 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`__&notification=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
View 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, &params)
```
!!! example "Example service URL"
zulip://my-bot%40zulipchat.com:correcthorsebatterystable@example.zulipchat.com?stream=foo&topic=bar

BIN
docs/shoutrrr-180px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/shoutrrr-logotype.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

269
docs/shoutrrr-logotype.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 144 KiB

BIN
docs/shoutrrr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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)
}
})
}
}

View 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 Gos 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 failures message, appending the wrapped errors 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 Gos 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{}

View 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
View 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`

View 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))
}

View 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
}

View 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,
}
}

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