├── .github └── workflows │ ├── build.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .typos.toml ├── LICENSE ├── README.md ├── _example └── usage │ └── main.go ├── consts.go ├── go.mod ├── go.sum ├── internal └── crypto │ ├── dingtalk.go │ └── lark.go ├── notifier.go ├── options.go ├── provider.go └── provider ├── bark └── bark.go ├── dingtalk └── dingtalk.go ├── feishu └── feishu.go ├── lark └── lark.go └── serverchan └── serverchan.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | go-version: ["1.22.x"] 17 | 18 | steps: 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Go ${{ matrix.go-version }} 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | cache: false 27 | id: go 28 | 29 | - name: cache go modules 30 | uses: actions/cache@v4 31 | with: 32 | path: | 33 | ~/go/pkg/mod 34 | ~/.cache/go-build 35 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 36 | restore-keys: | 37 | ${{ runner.os }}-go- 38 | 39 | - name: Format Check 40 | if: matrix.os != 'windows-latest' 41 | run: | 42 | diff -u <(echo -n) <(gofmt -d .) 43 | 44 | - name: Get dependencies 45 | run: go mod download 46 | 47 | - name: Build 48 | run: go build -v ./... 49 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set Golang 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.22.x" 21 | cache: false 22 | 23 | - name: Check spelling with custom config file 24 | uses: crate-ci/typos@master 25 | with: 26 | config: ./.typos.toml 27 | 28 | - name: Lint 29 | uses: golangci/golangci-lint-action@v3 30 | with: 31 | version: latest 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: ["1.22.x"] 14 | platform: [ubuntu-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Go 21 | if: success() 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | - name: Run tests 27 | run: go test -v ./... -covermode=count 28 | 29 | coverage: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Install Go 33 | if: success() 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: "1.22.x" 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | - name: Calc coverage 40 | run: | 41 | go test -v ./... -covermode=count -coverprofile=coverage.out 42 | 43 | - name: Convert coverage.out to coverage.lcov 44 | uses: jandelgado/gcov2lcov-action@v1 45 | 46 | - name: Coveralls 47 | uses: coverallsapp/github-action@v2 48 | with: 49 | github-token: ${{ secrets.github_token }} 50 | path-to-lcov: coverage.lcov 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | !.github/workflows/*.yml 17 | !.typos.toml 18 | !.golangci.yml -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: '5m' 3 | skip-dirs: 4 | - 'assets' 5 | allow-parallel-runners: true 6 | modules-download-mode: 'readonly' 7 | 8 | linters: 9 | enable: 10 | - 'asciicheck' 11 | - 'depguard' 12 | - 'dogsled' 13 | - 'errorlint' 14 | - 'exhaustive' 15 | - 'exportloopref' 16 | - 'gofmt' 17 | - 'goheader' 18 | - 'goimports' 19 | - 'gomodguard' 20 | - 'goprintffuncname' 21 | - 'gosec' 22 | - 'govet' 23 | - 'ineffassign' 24 | - 'makezero' 25 | - 'misspell' 26 | - 'prealloc' 27 | - 'predeclared' 28 | - 'revive' 29 | - 'typecheck' 30 | - 'unconvert' 31 | - 'whitespace' 32 | - 'forbidigo' 33 | - 'errcheck' 34 | - 'funlen' 35 | - 'gci' 36 | - 'gocritic' 37 | - 'godox' 38 | - 'sloglint' 39 | - 'usestdlibvars' 40 | 41 | disable: 42 | # unsupported lint with golang 1.18+ ref: https://github.com/golangci/golangci-lint/issues/2649 43 | - 'bodyclose' 44 | - 'gosimple' 45 | - 'noctx' 46 | - 'sqlclosecheck' 47 | - 'staticcheck' 48 | - 'structcheck' 49 | - 'stylecheck' 50 | - 'unused' 51 | - 'deadcode' 52 | - 'varcheck' 53 | - 'paralleltest' 54 | 55 | issues: 56 | exclude-use-default: false 57 | exclude: 58 | - should have a package comment 59 | - should have comment 60 | - G101 # Look for hard coded credentials 61 | - G102 # Bind to all interfaces 62 | - G103 # Audit the use of unsafe block 63 | - G104 # Audit errors not checked 64 | - G106 # Audit the use of ssh.InsecureIgnoreHostKey 65 | - G107 # Url provided to HTTP request as taint input 66 | - G108 # Profiling endpoint automatically exposed on /debug/pprof 67 | - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 68 | - G110 # Potential DoS vulnerability via decompression bomb 69 | - G111 # Potential directory traversal 70 | - G112 # Potential slowloris attack 71 | - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) 72 | - G114 # Use of net/http serve function that has no support for setting timeouts 73 | - G201 # SQL query construction using format string 74 | - G202 # SQL query construction using string concatenation 75 | - G203 # Use of unescaped data in HTML templates 76 | - G204 # Audit use of command execution 77 | - G301 # Poor file permissions used when creating a directory 78 | - G302 # Poor file permissions used with chmod 79 | - G303 # Creating tempfile using a predictable path 80 | - G304 # File path provided as taint input 81 | - G305 # File traversal when extracting zip/tar archive 82 | - G306 # Poor file permissions used when writing to a new file 83 | - G307 # Poor file permissions used when creating a file with os.Create 84 | - G401 # Detect the usage of DES, RC4, MD5 or SHA1 85 | - G402 # Look for bad TLS connection settings 86 | - G403 # Ensure minimum RSA key length of 2048 bits 87 | - G404 # Insecure random number source (rand) 88 | - G501 # Import blocklist: crypto/md5 89 | - G502 # Import blocklist: crypto/des 90 | - G503 # Import blocklist: crypto/rc4 91 | - G504 # Import blocklist: net/http/cgi 92 | - G505 # Import blocklist: crypto/sha1 93 | - G601 # Implicit memory aliasing of items from a range statement 94 | - G602 # Slice access out of bounds 95 | exclude-rules: 96 | - path: browser_bak/browser_bak\.go 97 | linters: 98 | - 'unused' 99 | max-issues-per-linter: 0 100 | max-same-issues: 0 101 | 102 | linters-settings: 103 | # Forbid the use of the following packages. 104 | depguard: 105 | rules: 106 | main: 107 | files: 108 | - $all 109 | deny: 110 | - pkg: "github.com/pkg/errors" 111 | desc: Should be replaced by standard lib errors package 112 | # Forbid the following identifiers (list of regexp). 113 | forbidigo: 114 | forbid: 115 | - ^print.*$ 116 | - p: ^fmt\.Print.*$ 117 | msg: Do not commit print statements. 118 | exclude-godoc-examples: true 119 | # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()). 120 | dogsled: 121 | max-blank-identifiers: 3 122 | errcheck: 123 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 124 | check-type-assertions: true 125 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. 126 | check-blank: false 127 | # List of functions to exclude from checking, where each entry is a single function to exclude. 128 | exclude-functions: 129 | - 'os.Remove' 130 | - 'os.RemoveAll' 131 | - '(*database/sql.DB).Close' 132 | - '(*database/sql.Rows).Close' 133 | - '(*github.com/syndtr/goleveldb/leveldb.DB).Close' 134 | exhaustive: 135 | # Program elements to check for exhaustiveness. 136 | # Default: [ switch ] 137 | check: 138 | - switch 139 | - map 140 | # Check switch statements in generated files also. 141 | # Default: false 142 | check-generated: true 143 | # Presence of "default" case in switch statements satisfies exhaustiveness, 144 | # even if all enum members are not listed. 145 | # Default: false 146 | default-signifies-exhaustive: true 147 | # Consider enums only in package scopes, not in inner scopes. 148 | # Default: false 149 | package-scope-only: true 150 | # Only run exhaustive check on switches with "//exhaustive:enforce" comment. 151 | # Default: false 152 | explicit-exhaustive-switch: true 153 | # Only run exhaustive check on map literals with "//exhaustive:enforce" comment. 154 | # Default: false 155 | explicit-exhaustive-map: true 156 | # Switch statement requires default case even if exhaustive. 157 | funlen: 158 | # Checks the number of lines in a function. 159 | # If lower than 0, disable the check. 160 | # Default: 60 161 | lines: 120 162 | # Checks the number of statements in a function. 163 | # If lower than 0, disable the check. 164 | # Default: 40 165 | statements: 50 166 | # Ignore comments when counting lines. 167 | # Default false 168 | ignore-comments: true 169 | gci: 170 | # DEPRECATED: use `sections` and `prefix(github.com/org/project)` instead. 171 | local-prefixes: github.com/moond4rk/go-template 172 | # Section configuration to compare against. 173 | # Section names are case-insensitive and may contain parameters in (). 174 | # The default order of sections is `standard > default > custom > blank > dot > alias`, 175 | # If `custom-order` is `true`, it follows the order of `sections` option. 176 | # Default: ["standard", "default"] 177 | sections: 178 | - standard # Standard section: captures all standard packages. 179 | - default # Default section: contains all imports that could not be matched to another section type. 180 | - prefix(github.com/moond4rk/go-template) # Custom section: groups all imports with the specified Prefix. 181 | - blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. 182 | - dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. 183 | - alias # Alias section: contains all alias imports. This section is not present unless explicitly enabled. 184 | # Skip generated files. 185 | # Default: true 186 | skip-generated: false 187 | # Enable custom order of sections. 188 | # If `true`, make the section order the same as the order of `sections`. 189 | # Default: false 190 | custom-order: true 191 | gocritic: 192 | # Which checks should be enabled; can't be combined with 'disabled-checks'. 193 | # See https://go-critic.github.io/overview#checks-overview. 194 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`. 195 | # By default, list of stable checks is used. 196 | enabled-checks: 197 | - nestingReduce 198 | - unnamedResult 199 | - ruleguard 200 | - captLocal 201 | - elseif 202 | - ifElseChain 203 | - rangeExprCopy 204 | - tooManyResultsChecker 205 | - truncateCmp 206 | - underef 207 | # Which checks should be disabled; can't be combined with 'enabled-checks'. 208 | # Default: [] 209 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 210 | # See https://github.com/go-critic/go-critic#usage -> section "Tags". 211 | # Default: [] 212 | enabled-tags: 213 | - diagnostic 214 | - style 215 | - performance 216 | - experimental 217 | - opinionated 218 | disabled-tags: 219 | - diagnostic 220 | - style 221 | - performance 222 | - experimental 223 | - opinionated 224 | # Settings passed to gocritic. 225 | # The settings key is the name of a supported gocritic checker. 226 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 227 | settings: 228 | # Must be valid enabled check name. 229 | captLocal: 230 | # Whether to restrict checker to params only. 231 | # Default: true 232 | paramsOnly: false 233 | elseif: 234 | # Whether to skip balanced if-else pairs. 235 | # Default: true 236 | skipBalanced: false 237 | ifElseChain: 238 | # Min number of if-else blocks that makes the warning trigger. 239 | # Default: 2 240 | minThreshold: 4 241 | nestingReduce: 242 | # Min number of statements inside a branch to trigger a warning. 243 | # Default: 5 244 | bodyWidth: 4 245 | rangeExprCopy: 246 | # Size in bytes that makes the warning trigger. 247 | # Default: 512 248 | sizeThreshold: 516 249 | # Whether to check test functions 250 | # Default: true 251 | skipTestFuncs: false 252 | tooManyResultsChecker: 253 | # Maximum number of results. 254 | # Default: 5 255 | maxResults: 10 256 | truncateCmp: 257 | skipArchDependent: false 258 | underef: 259 | # Whether to skip (*x).method() calls where x is a pointer receiver. 260 | # Default: true 261 | skipRecvDeref: false 262 | unnamedResult: 263 | # Whether to check exported functions. 264 | # Default: false 265 | checkExported: true 266 | godox: 267 | # Report any comments starting with keywords, this is useful for TODO or FIXME comments that 268 | # might be left in the code accidentally and should be resolved before merging. 269 | # Default: ["TODO", "BUG", "FIXME"] 270 | keywords: 271 | - NOTE 272 | - OPTIMIZE # marks code that should be optimized before merging 273 | - HACK # marks hack-around that should be removed before merging 274 | goimports: 275 | # A comma-separated list of prefixes, which, if set, checks import paths 276 | # with the given prefixes are grouped after 3rd-party packages. 277 | # Default: "" 278 | local-prefixes: github.com/moond4rk/go-template 279 | govet: 280 | # Report about shadowed variables. 281 | # Default: false 282 | check-shadowing: false 283 | # Settings per analyzer. 284 | settings: 285 | unusedresult: 286 | # Comma-separated list of functions whose results must be used 287 | # (in addition to default: 288 | # context.WithCancel, context.WithDeadline, context.WithTimeout, context.WithValue, errors.New, fmt.Errorf, 289 | # fmt.Sprint, fmt.Sprintf, sort.Reverse 290 | # ). 291 | # Default: [] 292 | enable-all: true 293 | disable: 294 | - 'fieldalignment' 295 | - 'shadow' 296 | sloglint: 297 | # Enforce not mixing key-value pairs and attributes. 298 | # Default: true 299 | no-mixed-args: false 300 | # Enforce using key-value pairs only (overrides no-mixed-args, incompatible with attr-only). 301 | # Default: false 302 | kv-only: true 303 | # Enforce using attributes only (overrides no-mixed-args, incompatible with kv-only). 304 | # Default: false 305 | # attr-only: true 306 | # Enforce using methods that accept a context. 307 | # Default: false 308 | context-only: false 309 | # Enforce using static values for log messages. 310 | # Default: false 311 | static-msg: true 312 | # Enforce using constants instead of raw keys. 313 | # Default: false 314 | no-raw-keys: false 315 | # Enforce a single key naming convention. 316 | # Values: snake, kebab, camel, pascal 317 | # Default: "" 318 | key-naming-case: snake 319 | # Enforce putting arguments on separate lines. 320 | # Default: false 321 | args-on-sep-lines: false 322 | usestdlibvars: 323 | # Suggest the use of http.MethodXX. 324 | # Default: true 325 | http-method: false 326 | # Suggest the use of http.StatusXX. 327 | # Default: true 328 | http-status-code: false 329 | # Suggest the use of time.Weekday.String(). 330 | # Default: true 331 | time-weekday: true 332 | # Suggest the use of time.Month.String(). 333 | # Default: false 334 | time-month: true 335 | # Suggest the use of time.Layout. 336 | # Default: false 337 | time-layout: true 338 | # Suggest the use of crypto.Hash.String(). 339 | # Default: false 340 | crypto-hash: true 341 | # Suggest the use of rpc.DefaultXXPath. 342 | # Default: false 343 | default-rpc-path: true 344 | # DEPRECATED Suggest the use of os.DevNull. 345 | # Default: false 346 | os-dev-null: true 347 | # Suggest the use of sql.LevelXX.String(). 348 | # Default: false 349 | sql-isolation-level: true 350 | # Suggest the use of tls.SignatureScheme.String(). 351 | # Default: false 352 | tls-signature-scheme: true 353 | # Suggest the use of constant.Kind.String(). 354 | # Default: false 355 | constant-kind: true 356 | # DEPRECATED Suggest the use of syslog.Priority. 357 | # Default: false 358 | syslog-priority: true -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos 2 | [default.extend-words] 3 | Encrypter = "Encrypter" 4 | Decrypter = "Decrypter" 5 | caf = "caf" 6 | [files] 7 | extend-exclude = ["go.mod", "go.sum"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ᴍᴏᴏɴD4ʀᴋ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notifier 2 | 3 | ![CI](https://github.com/moonD4rk/notifier/workflows/CI/badge.svg?branch=main) 4 | 5 | notifier is a simple Go library to send notification to other applications. 6 | 7 | ## Feature 8 | 9 | | Provider | Code | 10 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 11 | | [DingTalk](https://www.dingtalk.com/en) | [provider/dingtalk](https://github.com/moonD4rk/notifier/tree/main/provider/dingtalk) | 12 | | [Bark](https://apps.apple.com/us/app/bark-customed-notifications/id1403753865) | [provider/bark](https://github.com/moonD4rk/notifier/tree/main/provider/bark) | 13 | | [Lark](https://www.larksuite.com/en_us/) | [provider/lark](https://github.com/moonD4rk/notifier/tree/main/provider/lark) | 14 | | [Feishu](https://www.feishu.cn/) | [provider/feishu](https://github.com/moonD4rk/notifier/tree/main/provider/feishu) | 15 | | [Server 酱](https://sct.ftqq.com/) | [provider/serverchan](https://github.com/moonD4rk/notifier/tree/main/provider/serverchan) | 16 | 17 | ## Install 18 | 19 | `go get -u github.com/moond4rk/notifier` 20 | 21 | ## Usage 22 | 23 | 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "os" 30 | 31 | "github.com/moond4rk/notifier" 32 | ) 33 | 34 | func main() { 35 | var ( 36 | dingtalkToken = os.Getenv("dingtalk_token") 37 | dingtalkSecret = os.Getenv("dingtalk_secret") 38 | barkKey = os.Getenv("bark_key") 39 | barkServer = notifier.DefaultBarkServer 40 | feishuToken = os.Getenv("feishu_token") 41 | feishuSecret = os.Getenv("feishu_secret") 42 | larkToken = os.Getenv("lark_token") 43 | larkSecret = os.Getenv("lark_secret") 44 | serverChanUserID = "" // server chan's userID could be empty 45 | serverChanSendKey = os.Getenv("server_chan_send_key") 46 | ) 47 | notifier := notifier.New( 48 | notifier.WithDingTalk(dingtalkToken, dingtalkSecret), 49 | notifier.WithBark(barkKey, barkServer), 50 | notifier.WithFeishu(feishuToken, feishuSecret), 51 | notifier.WithLark(larkToken, larkSecret), 52 | notifier.WithServerChan(serverChanUserID, serverChanSendKey), 53 | ) 54 | 55 | var ( 56 | subject = "this is subject" 57 | content = "this is content" 58 | ) 59 | if err := notifier.Send(subject, content); err != nil { 60 | panic(err) 61 | } 62 | } 63 | ``` 64 | 65 | 66 | -------------------------------------------------------------------------------- /_example/usage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/moond4rk/notifier" 7 | ) 8 | 9 | func main() { 10 | var ( 11 | dingtalkToken = os.Getenv("dingtalk_token") 12 | dingtalkSecret = os.Getenv("dingtalk_secret") 13 | barkKey = os.Getenv("bark_key") 14 | barkServer = notifier.DefaultBarkServer 15 | feishuToken = os.Getenv("feishu_token") 16 | feishuSecret = os.Getenv("feishu_secret") 17 | larkToken = os.Getenv("lark_token") 18 | larkSecret = os.Getenv("lark_secret") 19 | serverChanUserID = "" // server chan's userID could be empty 20 | serverChanSendKey = os.Getenv("server_chan_send_key") 21 | ) 22 | notifier := notifier.New( 23 | notifier.WithDingTalk(dingtalkToken, dingtalkSecret), 24 | notifier.WithBark(barkKey, barkServer), 25 | notifier.WithFeishu(feishuToken, feishuSecret), 26 | notifier.WithLark(larkToken, larkSecret), 27 | notifier.WithServerChan(serverChanUserID, serverChanSendKey), 28 | ) 29 | 30 | var ( 31 | subject = "this is subject" 32 | content = "this is content" 33 | ) 34 | if err := notifier.Send(subject, content); err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | const ( 4 | // DefaultBarkServer is the default bark server domain 5 | // more info: https://day.app/2018/06/bark-server-document/ 6 | DefaultBarkServer = "api.day.app" 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moond4rk/notifier 2 | 3 | go 1.22 4 | 5 | require golang.org/x/sync v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 2 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 3 | -------------------------------------------------------------------------------- /internal/crypto/dingtalk.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | const defaultDingTalkURL = `https://oapi.dingtalk.com/robot/send` 15 | 16 | func DingTalkURL(token, secret string) (string, error) { 17 | timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10) 18 | u, _ := url.Parse(defaultDingTalkURL) 19 | value := url.Values{} 20 | value.Set("access_token", token) 21 | 22 | if secret == "" { 23 | u.RawQuery = value.Encode() 24 | return u.String(), nil 25 | } 26 | 27 | sign, err := dingTalkSign(timestamp, secret) 28 | if err != nil { 29 | u.RawQuery = value.Encode() 30 | return u.String(), err 31 | } 32 | 33 | value.Set("timestamp", timestamp) 34 | value.Set("sign", sign) 35 | u.RawQuery = value.Encode() 36 | return u.String(), nil 37 | } 38 | 39 | func dingTalkSign(timestamp, secret string) (string, error) { 40 | stringToSign := fmt.Sprintf("%s\n%s", timestamp, secret) 41 | h := hmac.New(sha256.New, []byte(secret)) 42 | if _, err := io.WriteString(h, stringToSign); err != nil { 43 | return "", err 44 | } 45 | return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/crypto/lark.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | // LarkData generate sign for lark 13 | func LarkData(subject, content, secret string) ([]byte, error) { 14 | var ( 15 | sign string 16 | err error 17 | ) 18 | timestamp := fmt.Sprintf("%d", time.Now().Unix()) 19 | if secret != "" { 20 | sign, err = larkSign(timestamp, secret) 21 | if err != nil { 22 | return nil, err 23 | } 24 | } 25 | data, err := buildLarkPostData(subject, content, timestamp, sign) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return data, nil 30 | } 31 | 32 | func larkSign(timestamp, secret string) (string, error) { 33 | key := fmt.Sprintf("%s\n%s", timestamp, secret) 34 | var data []byte 35 | h := hmac.New(sha256.New, []byte(key)) 36 | _, err := h.Write(data) 37 | if err != nil { 38 | return "", err 39 | } 40 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 41 | return signature, nil 42 | } 43 | 44 | func buildLarkPostData(subject, content, timestamp, sign string) ([]byte, error) { 45 | msg := contentMsg{ 46 | Tag: "text", 47 | Text: content, 48 | } 49 | pd := &postData{ 50 | Timestamp: timestamp, 51 | Sign: sign, 52 | MsgType: "post", 53 | } 54 | pd.Content.Post.ZhCN.Title = subject 55 | pd.Content.Post.ZhCN.Content = append(pd.Content.Post.ZhCN.Content, []contentMsg{msg}) 56 | data, err := json.Marshal(pd) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return data, err 61 | } 62 | 63 | type postData struct { 64 | Timestamp string `json:"timestamp"` 65 | Sign string `json:"sign,omitempty"` 66 | MsgType string `json:"msg_type"` 67 | Content content `json:"content"` 68 | } 69 | 70 | type content struct { 71 | Post post `json:"post"` 72 | } 73 | 74 | type zhCN struct { 75 | Title string `json:"title"` 76 | Content [][]contentMsg `json:"content"` 77 | } 78 | 79 | type post struct { 80 | ZhCN zhCN `json:"zh_cn,omitempty"` 81 | } 82 | 83 | type contentMsg struct { 84 | Tag string `json:"tag"` 85 | Text string `json:"text"` 86 | } 87 | -------------------------------------------------------------------------------- /notifier.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "errors" 5 | 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | type Notifier struct { 10 | Providers []provider 11 | } 12 | 13 | func New(options ...Option) *Notifier { 14 | n := &Notifier{Providers: []provider{}} 15 | for _, option := range options { 16 | option(n) 17 | } 18 | return n 19 | } 20 | 21 | var ( 22 | ErrSendNotification = errors.New("send notification") 23 | ErrNoProvider = errors.New("no provider, please check your config") 24 | ) 25 | 26 | func (n *Notifier) Send(subject, content string) error { 27 | if len(n.Providers) == 0 { 28 | return ErrNoProvider 29 | } 30 | var eg errgroup.Group 31 | for _, provider := range n.Providers { 32 | p := provider 33 | eg.Go(func() error { 34 | return p.Send(subject, content) 35 | }) 36 | } 37 | err := eg.Wait() 38 | if err != nil { 39 | err = errors.Join(ErrSendNotification, err) 40 | } 41 | 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "github.com/moond4rk/notifier/provider/bark" 5 | "github.com/moond4rk/notifier/provider/dingtalk" 6 | "github.com/moond4rk/notifier/provider/feishu" 7 | "github.com/moond4rk/notifier/provider/lark" 8 | "github.com/moond4rk/notifier/provider/serverchan" 9 | ) 10 | 11 | type Option func(p *Notifier) 12 | 13 | func WithDingTalk(token, secret string) Option { 14 | d := dingtalk.New(token, secret) 15 | return func(n *Notifier) { 16 | if d != nil { 17 | n.Providers = append(n.Providers, d) 18 | } 19 | } 20 | } 21 | 22 | func WithBark(key, server string) Option { 23 | b := bark.New(key, server) 24 | return func(n *Notifier) { 25 | if b != nil { 26 | n.Providers = append(n.Providers, b) 27 | } 28 | } 29 | } 30 | 31 | func WithLark(token, secret string) Option { 32 | l := lark.New(token, secret) 33 | return func(n *Notifier) { 34 | if l != nil { 35 | n.Providers = append(n.Providers, l) 36 | } 37 | } 38 | } 39 | 40 | func WithFeishu(token, secret string) Option { 41 | l := feishu.New(token, secret) 42 | return func(n *Notifier) { 43 | if l != nil { 44 | n.Providers = append(n.Providers, l) 45 | } 46 | } 47 | } 48 | 49 | func WithServerChan(userID, sendKey string) Option { 50 | s := serverchan.New(userID, sendKey) 51 | return func(n *Notifier) { 52 | if s != nil { 53 | n.Providers = append(n.Providers, s) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | type provider interface { 4 | Send(subject, content string) error 5 | } 6 | -------------------------------------------------------------------------------- /provider/bark/bark.go: -------------------------------------------------------------------------------- 1 | package bark 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | type Provider struct { 13 | Server string 14 | Key string 15 | link string 16 | } 17 | 18 | func New(key, server string) *Provider { 19 | if key == "" { 20 | return nil 21 | } 22 | p := &Provider{ 23 | Server: server, 24 | Key: key, 25 | } 26 | if server != "" { 27 | p.link = fmt.Sprintf("https://%s/push", server) 28 | } else { 29 | p.link = fmt.Sprintf("https://%s/push", "api.day.app") 30 | } 31 | return p 32 | } 33 | 34 | func (p *Provider) Send(subject, content string) error { 35 | type postData struct { 36 | DeviceKey string `json:"device_key"` 37 | Title string `json:"title"` 38 | Body string `json:"body,omitempty"` 39 | Badge int `json:"badge,omitempty"` 40 | Sound string `json:"sound,omitempty"` 41 | Icon string `json:"icon,omitempty"` 42 | Group string `json:"group,omitempty"` 43 | URL string `json:"link,omitempty"` 44 | } 45 | pd := &postData{ 46 | DeviceKey: p.Key, 47 | Title: subject, 48 | Body: content, 49 | Sound: "alarm.caf", 50 | } 51 | data, err := json.Marshal(pd) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | resp, err := http.Post(p.link, "application/json; charset=utf-8", bytes.NewReader(data)) 57 | if err != nil { 58 | return fmt.Errorf("send bark request failed %w", err) 59 | } 60 | result, err := io.ReadAll(resp.Body) 61 | if err != nil { 62 | return err 63 | } 64 | defer func() { 65 | // Close the body and check for errors 66 | if cerr := resp.Body.Close(); cerr != nil { 67 | // Handle the error, log it, etc. Here we're just logging. 68 | log.Printf("failed to close response body: %v", cerr) 69 | } 70 | }() 71 | 72 | if resp.StatusCode != http.StatusOK { 73 | err = fmt.Errorf("statusCode: %d, body: %v", resp.StatusCode, string(result)) 74 | return fmt.Errorf("send bark message failed %w", err) 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /provider/dingtalk/dingtalk.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/moond4rk/notifier/internal/crypto" 12 | ) 13 | 14 | type Provider struct { 15 | Token string `yaml:"token,omitempty"` 16 | Secret string `yaml:"secret,omitempty"` 17 | } 18 | 19 | func New(token, secret string) *Provider { 20 | if token == "" { 21 | return nil 22 | } 23 | return &Provider{ 24 | Token: token, 25 | Secret: secret, 26 | } 27 | } 28 | 29 | func (p *Provider) Send(subject, content string) error { 30 | data, err := buildPostData(subject, content) 31 | if err != nil { 32 | return fmt.Errorf("failed to create message %w", err) 33 | } 34 | link, err := crypto.DingTalkURL(p.Token, p.Secret) 35 | if err != nil { 36 | return fmt.Errorf("build dingtalk link error %w", err) 37 | } 38 | resp, err := http.Post(link, "application/json; charset=utf-8", bytes.NewReader(data)) 39 | if err != nil { 40 | return fmt.Errorf("send dingtalk request failed %w", err) 41 | } 42 | result, err := io.ReadAll(resp.Body) 43 | if err != nil { 44 | return err 45 | } 46 | defer func() { 47 | // Close the body and check for errors 48 | if cerr := resp.Body.Close(); cerr != nil { 49 | // Handle the error, log it, etc. Here we're just logging. 50 | log.Printf("failed to close response body: %v", cerr) 51 | } 52 | }() 53 | 54 | if resp.StatusCode != http.StatusOK { 55 | err = fmt.Errorf("statusCode: %d, body: %v", resp.StatusCode, string(result)) 56 | return fmt.Errorf("dingtalk message response error %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func buildPostData(subject, content string) ([]byte, error) { 62 | content = fmt.Sprintf("### %s\n>%s", subject, content) 63 | type postData struct { 64 | MsgType string `json:"msgtype"` 65 | Markdown struct { 66 | Title string `json:"title"` 67 | Text string `json:"text"` 68 | } `json:"markdown"` 69 | } 70 | pd := &postData{MsgType: "markdown"} 71 | pd.Markdown.Title = subject 72 | pd.Markdown.Text = content 73 | data, err := json.Marshal(pd) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return data, err 78 | } 79 | -------------------------------------------------------------------------------- /provider/feishu/feishu.go: -------------------------------------------------------------------------------- 1 | package feishu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/moond4rk/notifier/internal/crypto" 12 | ) 13 | 14 | type Provider struct { 15 | Token string 16 | Secret string 17 | } 18 | 19 | func New(token, secret string) *Provider { 20 | if token == "" { 21 | return nil 22 | } 23 | return &Provider{ 24 | Token: token, 25 | Secret: secret, 26 | } 27 | } 28 | 29 | func (p *Provider) Send(subject, content string) error { 30 | data, err := crypto.LarkData(subject, content, p.Secret) 31 | 32 | link := fmt.Sprintf("https://open.feishu.cn/open-apis/bot/v2/hook/%s", p.Token) 33 | if err != nil { 34 | return fmt.Errorf("build dingtalk link error %w", err) 35 | } 36 | resp, err := http.Post(link, "application/json; charset=utf-8", bytes.NewReader(data)) 37 | if err != nil { 38 | return fmt.Errorf("send dingtalk request failed %w", err) 39 | } 40 | result, err := io.ReadAll(resp.Body) 41 | if err != nil { 42 | return err 43 | } 44 | defer func() { 45 | // Close the body and check for errors 46 | if cerr := resp.Body.Close(); cerr != nil { 47 | // Handle the error, log it, etc. Here we're just logging. 48 | log.Printf("failed to close response body: %v", cerr) 49 | } 50 | }() 51 | type response struct { 52 | Code int `json:"code"` 53 | Data struct{} `json:"data"` 54 | Msg string `json:"msg"` 55 | Extra interface{} `json:"Extra"` 56 | StatusCode int `json:"StatusCode"` 57 | StatusMessage string `json:"StatusMessage"` 58 | } 59 | 60 | var r response 61 | if err = json.Unmarshal(result, &r); err != nil { 62 | return fmt.Errorf("parse feishu response failed %w", err) 63 | } 64 | 65 | if r.StatusMessage != "success" { 66 | return fmt.Errorf("feishu message response error %w", fmt.Errorf("body: %v", string(result))) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /provider/lark/lark.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/moond4rk/notifier/internal/crypto" 12 | ) 13 | 14 | type Provider struct { 15 | Token string 16 | Secret string 17 | } 18 | 19 | func New(token, secret string) *Provider { 20 | if token == "" { 21 | return nil 22 | } 23 | return &Provider{ 24 | Token: token, 25 | Secret: secret, 26 | } 27 | } 28 | 29 | func (p *Provider) Send(subject, content string) error { 30 | data, err := crypto.LarkData(subject, content, p.Secret) 31 | link := fmt.Sprintf("https://open.larksuite.com/open-apis/bot/v2/hook/%s", p.Token) 32 | if err != nil { 33 | return fmt.Errorf("build dingtalk link %w", err) 34 | } 35 | resp, err := http.Post(link, "application/json; charset=utf-8", bytes.NewReader(data)) 36 | if err != nil { 37 | return fmt.Errorf("send dingtalk request failed %w", err) 38 | } 39 | result, err := io.ReadAll(resp.Body) 40 | if err != nil { 41 | return err 42 | } 43 | defer func() { 44 | // Close the body and check for errors 45 | if cerr := resp.Body.Close(); cerr != nil { 46 | // Handle the error, log it, etc. Here we're just logging. 47 | log.Printf("failed to close response body: %v", cerr) 48 | } 49 | }() 50 | 51 | type response struct { 52 | Code int `json:"code"` 53 | Data struct{} `json:"data"` 54 | Msg string `json:"msg"` 55 | Extra interface{} `json:"Extra"` 56 | StatusCode int `json:"StatusCode"` 57 | StatusMessage string `json:"StatusMessage"` 58 | } 59 | 60 | var r response 61 | if err = json.Unmarshal(result, &r); err != nil { 62 | return fmt.Errorf("parse lark response failed %w", err) 63 | } 64 | 65 | if r.StatusMessage != "success" { 66 | return fmt.Errorf("lark message response %w", fmt.Errorf("body: %v", string(result))) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /provider/serverchan/serverchan.go: -------------------------------------------------------------------------------- 1 | package serverchan 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | type Provider struct { 12 | UserID string 13 | SendKey string 14 | } 15 | 16 | func New(userID, sendKey string) *Provider { 17 | if sendKey == "" { 18 | return nil 19 | } 20 | return &Provider{ 21 | UserID: userID, 22 | SendKey: sendKey, 23 | } 24 | } 25 | 26 | var defaultHost = "sctapi.ftqq.com" 27 | 28 | func (p *Provider) Send(subject, content string) error { 29 | link := fmt.Sprintf("https://%s/%s.send", defaultHost, p.SendKey) 30 | type postData struct { 31 | Text string `json:"text"` 32 | Desp string `json:"desp"` 33 | } 34 | pd := &postData{ 35 | Text: subject, 36 | Desp: content, 37 | } 38 | data, err := json.Marshal(pd) 39 | if err != nil { 40 | return fmt.Errorf("json marshal %w", err) 41 | } 42 | resp, err := http.Post(link, "application/json; charset=utf-8", bytes.NewReader(data)) 43 | if err != nil { 44 | return fmt.Errorf("http post %w", err) 45 | } 46 | defer func() { 47 | // Close the body and check for errors 48 | if cerr := resp.Body.Close(); cerr != nil { 49 | // Handle the error, log it, etc. Here we're just logging. 50 | log.Printf("failed to close response body: %v", cerr) 51 | } 52 | }() 53 | if resp.StatusCode != 200 { 54 | return fmt.Errorf("send server chan failed %w", fmt.Errorf("http status code: %d", resp.StatusCode)) 55 | } 56 | return nil 57 | } 58 | --------------------------------------------------------------------------------