Compare commits

...

3 commits

Author SHA1 Message Date
421e85cb0b refactor: extract alerts into own actor
All checks were successful
Rust / build (push) Successful in 1m42s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
2024-08-03 12:59:40 +01:00
9a2fa2e8a5 feat: add support for desktop notifications
All checks were successful
Rust / build (push) Successful in 1m46s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Closes: kemitix/git-next#119
2024-08-03 12:59:40 +01:00
2b77eae508 build: update to rust with libdbus-1-dev
All checks were successful
Rust / build (push) Successful in 3m10s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
2024-08-03 12:59:40 +01:00
26 changed files with 574 additions and 125 deletions

View file

@ -18,21 +18,21 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Format - name: Format
uses: https://git.kemitix.net/kemitix/rust@v1.80.0 uses: https://git.kemitix.net/kemitix/rust@v1.80.0-1
with: with:
args: fmt --all -- --check args: fmt --all -- --check
- name: Clippy - name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v1.80.0 uses: https://git.kemitix.net/kemitix/rust@v1.80.0-1
with: with:
args: clippy args: clippy
- name: Build - name: Build
uses: https://git.kemitix.net/kemitix/rust@v1.80.0 uses: https://git.kemitix.net/kemitix/rust@v1.80.0-1
with: with:
args: build args: build
- name: Test - name: Test
uses: https://git.kemitix.net/kemitix/rust@v1.80.0 uses: https://git.kemitix.net/kemitix/rust@v1.80.0-1
with: with:
args: test args: test

273
Cargo.lock generated
View file

@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.14",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@ -102,6 +102,21 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.13" version = "0.6.13"
@ -162,6 +177,18 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arrayref"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.4" version = "0.7.4"
@ -224,6 +251,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@ -248,6 +281,23 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "blake2b_simd"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
"arrayvec 0.5.2",
"constant_time_eq",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -304,6 +354,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "chumsky" name = "chumsky"
version = "0.9.3" version = "0.9.3"
@ -393,6 +457,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -512,6 +582,16 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "dbus"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b5f0f36f1eebe901b0e6bee369a77ed3396334bf3f09abd46454a576f71819"
dependencies = [
"libc",
"libdbus-sys",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -570,6 +650,17 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "dirs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"
@ -805,6 +896,17 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.14" version = "0.2.14"
@ -814,7 +916,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -856,6 +958,7 @@ dependencies = [
"kxio", "kxio",
"lettre", "lettre",
"mockall", "mockall",
"notifica",
"notify", "notify",
"pretty_assertions", "pretty_assertions",
"rand", "rand",
@ -2138,6 +2241,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -2332,6 +2458,15 @@ version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "libdbus-sys"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
dependencies = [
"pkg-config",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.13" version = "0.4.13"
@ -2354,6 +2489,27 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "mac-notification-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76"
dependencies = [
"cc",
"chrono",
"dirs",
"objc-foundation",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -2428,7 +2584,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2504,6 +2660,17 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "notifica"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e81fcdf9755383979b66adf525a66a8f621b55882a820552b201839b0ce3f7"
dependencies = [
"mac-notification-sys",
"notify-rust",
"winrt",
]
[[package]] [[package]]
name = "notify" name = "notify"
version = "6.1.1" version = "6.1.1"
@ -2523,6 +2690,16 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "notify-rust"
version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8da29142be3f71b2165a6b3991c26045b674edbf04cdfc42f323094fc3e4b5a"
dependencies = [
"dbus",
"mac-notification-sys",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -2539,6 +2716,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -2558,6 +2744,35 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -2832,7 +3047,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
] ]
[[package]] [[package]]
@ -2855,6 +3070,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@ -2873,6 +3094,17 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
] ]
[[package]]
name = "redox_users"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom 0.1.16",
"redox_syscall 0.1.57",
"rust-argon2",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.4" version = "1.10.4"
@ -2973,13 +3205,25 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.14",
"libc", "libc",
"spin", "spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rust-argon2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64 0.13.1",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
@ -3770,7 +4014,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259" checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
"rand", "rand",
"web-time", "web-time",
] ]
@ -3781,7 +4025,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
dependencies = [ dependencies = [
"arrayvec", "arrayvec 0.7.4",
] ]
[[package]] [[package]]
@ -3921,6 +4165,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -4230,6 +4480,15 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "winrt"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c248f437add7df81d305a345e9d143c8c0a9de00a51e46b42453c337181d16c9"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.20" version = "0.8.20"

View file

@ -96,6 +96,9 @@ tokio = { version = "1.37", features = ["rt", "macros"] }
lettre = "0.11" lettre = "0.11"
sendmail = "2.0" sendmail = "2.0"
# desktop notifications
notifica = "3.0"
# Testing # Testing
assert2 = "0.3" assert2 = "0.3"
pretty_assertions = "1.4" pretty_assertions = "1.4"

View file

@ -70,6 +70,9 @@ notify = { workspace = true }
lettre = { workspace = true } lettre = { workspace = true }
sendmail = { workspace = true } sendmail = { workspace = true }
# desktop notifications
notifica = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Testing # Testing
assert2 = { workspace = true } assert2 = { workspace = true }

View file

@ -29,6 +29,8 @@ See [Behaviour](#behaviour) to learn how we do this.
- Rust 1.76.0 or later - https://www.rust-lang.org - Rust 1.76.0 or later - https://www.rust-lang.org
- pgk-config - pgk-config
- libssl-dev - libssl-dev
- libdbus-1-dev (ubuntu/debian)
- dbus-devel (fedora)
### x86_64-unknown-linux-gnu ### x86_64-unknown-linux-gnu
@ -125,6 +127,9 @@ forge is running on.
The server should be able to notify the user when manual intervention is required. The server should be able to notify the user when manual intervention is required.
```toml ```toml
[shoult]
desktop = true
[shout.webhook] [shout.webhook]
url = "https//localhost:9090" url = "https//localhost:9090"
secret = "secret-password" secret = "secret-password"
@ -139,6 +144,10 @@ username = "git-next@example.com"
password = "MySecretEmailPassword42" password = "MySecretEmailPassword42"
``` ```
##### desktop
When specified as `true`, desktop notifications will be sent for some events.
##### webhook ##### webhook
Will send a POST request for some events. Will send a POST request for some events.

View file

@ -0,0 +1,9 @@
//
use crate::alerts::{messages::NotifyUser, short_message};
pub(super) fn send_desktop_notification(msg: &NotifyUser) {
let message = short_message(msg);
if let Err(err) = notifica::notify("git-next", &message) {
tracing::warn!(?err, "failed to send desktop notification");
}
}

View file

@ -0,0 +1,68 @@
//
use git_next_core::server::{EmailConfig, SmtpConfig};
use crate::alerts::{full_message, messages::NotifyUser, short_message};
#[derive(Debug)]
struct EmailMessage {
from: String,
to: String,
subject: String,
body: String,
}
pub(super) fn send_email(msg: &NotifyUser, email_config: &EmailConfig) {
let email_message = EmailMessage {
from: email_config.from().to_string(),
to: email_config.to().to_string(),
subject: short_message(msg),
body: full_message(msg),
};
match email_config.smtp() {
Some(smtp) => send_email_smtp(email_message, smtp),
None => send_email_sendmail(email_message),
}
}
fn send_email_sendmail(email_message: EmailMessage) {
use sendmail::email;
match email::send(
&email_message.from,
[email_message.to.as_ref()],
&email_message.subject,
&email_message.body,
) {
Ok(_) => tracing::info!("Email sent successfully!"),
Err(e) => tracing::warn!("Could not send email: {:?}", e),
}
}
fn send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) {
if let Err(err) = do_send_email_smtp(email_message, smtp) {
tracing::warn!(?err, "sending email");
}
}
fn do_send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) -> Result<(), anyhow::Error> {
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
let email = Message::builder()
.from(email_message.from.parse()?)
.to(email_message.to.parse()?)
.subject(email_message.subject)
.body(email_message.body)?;
let creds = Credentials::new(smtp.username().to_string(), smtp.password().to_string());
let mailer = SmtpTransport::relay(smtp.hostname())?
.credentials(creds)
.build();
Ok(mailer
.send(&email)
.map(|response| {
response
.message()
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.map(|response| {
tracing::info!(?response, "email sent via smtp");
})?)
}

View file

@ -0,0 +1,2 @@
mod notify_user;
mod update_shout;

View file

@ -0,0 +1,36 @@
//
use actix::prelude::*;
use tracing::{info, instrument, Instrument as _};
use crate::alerts::{
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
webhook::send_webhook, AlertsActor,
};
impl Handler<NotifyUser> for AlertsActor {
type Result = ();
#[instrument]
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
let Some(shout) = &self.shout else {
info!("No shout config available");
return;
};
let net = self.net.clone();
let shout = shout.clone();
async move {
if let Some(webhook_config) = shout.webhook() {
send_webhook(&msg, webhook_config, &net).await;
}
if let Some(email_config) = shout.email() {
send_email(&msg, email_config);
}
if shout.desktop() {
send_desktop_notification(&msg);
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
}

View file

@ -0,0 +1,12 @@
//
use actix::prelude::*;
use crate::alerts::{messages::UpdateShout, AlertsActor};
impl Handler<UpdateShout> for AlertsActor {
type Result = ();
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
self.shout.replace(msg.unwrap());
}
}

View file

@ -0,0 +1,2 @@
#[derive(Debug, Default)]
pub struct History {}

View file

@ -0,0 +1,86 @@
//
use derive_more::Deref as _;
use git_next_core::{git::UserNotification, message, server::Shout};
use serde_json::json;
message!(UpdateShout: Shout: "Updated Shout configuration");
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook");
impl NotifyUser {
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
let timestamp = timestamp.unix_timestamp().to_string();
match self.deref() {
UserNotification::CICheckFailed {
forge_alias,
repo_alias,
commit,
} => json!({
"type": "cicheck.failed",
"timestamp": timestamp,
"data": {
"forge_alias": forge_alias,
"repo_alias": repo_alias,
"commit": {
"sha": commit.sha(),
"message": commit.message()
}
}
}),
UserNotification::RepoConfigLoadFailure {
forge_alias,
repo_alias,
reason,
} => json!({
"type": "config.load.failed",
"timestamp": timestamp,
"data": {
"forge_alias": forge_alias,
"repo_alias": repo_alias,
"reason": reason
}
}),
UserNotification::WebhookRegistration {
forge_alias,
repo_alias,
reason,
} => json!({
"type": "webhook.registration.failed",
"timestamp": timestamp,
"data": {
"forge_alias": forge_alias,
"repo_alias": repo_alias,
"reason": reason
}
}),
UserNotification::DevNotBasedOnMain {
forge_alias,
repo_alias,
dev_branch,
main_branch,
dev_commit,
main_commit,
} => json!({
"type": "branch.dev.not-on-main",
"timestamp": timestamp,
"data": {
"forge_alias": forge_alias,
"repo_alias": repo_alias,
"branches": {
"dev": dev_branch,
"main": main_branch
},
"commits": {
"dev": {
"sha": dev_commit.sha(),
"message": dev_commit.message()
},
"main": {
"sha": main_commit.sha(),
"message": main_commit.message()
}
}
}
}),
}
}
}

View file

@ -1,77 +1,35 @@
use std::ops::Deref as _; use std::ops::Deref as _;
use git_next_core::{ //
git::UserNotification, use actix::prelude::*;
server::{EmailConfig, SmtpConfig},
};
use crate::repo::messages::NotifyUser; use derive_more::derive::Constructor;
#[derive(Debug)] use git_next_core::{git::UserNotification, server::Shout};
struct EmailMessage {
from: String, pub use history::History;
to: String, use messages::NotifyUser;
subject: String,
body: String, mod desktop;
mod email;
mod handlers;
mod history;
pub mod messages;
mod webhook;
#[derive(Debug, Constructor)]
pub struct AlertsActor {
shout: Option<Shout>, // config for sending alerts to users
#[allow(dead_code)] // TODO (#128) Prevent duplicate user notifications
history: History, // record of alerts sent recently (e.g. 24 hours)
net: kxio::network::Network,
} }
pub(super) fn send_email(msg: &NotifyUser, email_config: &EmailConfig) { impl Actor for AlertsActor {
let email_message = EmailMessage { type Context = Context<Self>;
from: email_config.from().to_string(),
to: email_config.to().to_string(),
subject: email_subject(msg),
body: email_body(msg),
};
match email_config.smtp() {
Some(smtp) => send_email_smtp(email_message, smtp),
None => send_email_sendmail(email_message),
}
} }
fn send_email_sendmail(email_message: EmailMessage) { fn short_message(msg: &NotifyUser) -> String {
use sendmail::email;
match email::send(
&email_message.from,
[email_message.to.as_ref()],
&email_message.subject,
&email_message.body,
) {
Ok(_) => tracing::info!("Email sent successfully!"),
Err(e) => tracing::warn!("Could not send email: {:?}", e),
}
}
fn send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) {
if let Err(err) = do_send_email_smtp(email_message, smtp) {
tracing::warn!(?err, "sending email");
}
}
fn do_send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) -> Result<(), anyhow::Error> {
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
let email = Message::builder()
.from(email_message.from.parse()?)
.to(email_message.to.parse()?)
.subject(email_message.subject)
.body(email_message.body)?;
let creds = Credentials::new(smtp.username().to_string(), smtp.password().to_string());
let mailer = SmtpTransport::relay(smtp.hostname())?
.credentials(creds)
.build();
Ok(mailer
.send(&email)
.map(|response| {
response
.message()
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.map(|response| {
tracing::info!(?response, "email sent via smtp");
})?)
}
fn email_subject(msg: &NotifyUser) -> String {
let tail = match msg.deref() { let tail = match msg.deref() {
UserNotification::CICheckFailed { UserNotification::CICheckFailed {
forge_alias, forge_alias,
@ -100,7 +58,7 @@ fn email_subject(msg: &NotifyUser) -> String {
format!("[git-next] {tail}") format!("[git-next] {tail}")
} }
fn email_body(msg: &NotifyUser) -> String { fn full_message(msg: &NotifyUser) -> String {
match msg.deref() { match msg.deref() {
UserNotification::CICheckFailed { UserNotification::CICheckFailed {
forge_alias, forge_alias,

View file

@ -3,7 +3,7 @@ use git_next_core::server::OutboundWebhook;
use secrecy::ExposeSecret as _; use secrecy::ExposeSecret as _;
use standardwebhooks::Webhook; use standardwebhooks::Webhook;
use crate::repo::messages::NotifyUser; use crate::alerts::messages::NotifyUser;
pub(super) async fn send_webhook( pub(super) async fn send_webhook(
msg: &NotifyUser, msg: &NotifyUser,

View file

@ -1,4 +1,5 @@
// //
mod alerts;
mod file_watcher; mod file_watcher;
mod forge; mod forge;
mod init; mod init;

View file

@ -1,9 +1,9 @@
// //
use actix::prelude::*; use actix::prelude::*;
use crate::alerts::messages::NotifyUser;
use derive_more::Deref; use derive_more::Deref;
use kxio::network::Network; use kxio::network::Network;
use messages::NotifyUser;
use std::time::Duration; use std::time::Duration;
use tracing::{info, warn, Instrument}; use tracing::{info, warn, Instrument};

View file

@ -1,5 +1,4 @@
mod file_updated; mod file_updated;
mod notify_user;
mod receive_server_config; mod receive_server_config;
mod receive_valid_server_config; mod receive_valid_server_config;
mod shutdown; mod shutdown;

View file

@ -1,34 +0,0 @@
//
mod email;
mod webhook;
use actix::prelude::*;
use email::send_email;
use tracing::Instrument;
use webhook::send_webhook;
use crate::{repo::messages::NotifyUser, server::actor::ServerActor};
impl Handler<NotifyUser> for ServerActor {
type Result = ();
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
let Some(server_config) = &self.server_config else {
return;
};
let shout_config = server_config.shout().clone();
let net = self.net.clone();
async move {
if let Some(webhook_config) = shout_config.webhook() {
send_webhook(&msg, webhook_config, &net).await;
}
if let Some(email_config) = shout_config.email() {
send_email(&msg, email_config);
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
}

View file

@ -4,6 +4,7 @@ use actix::prelude::*;
use tracing::info; use tracing::info;
use crate::{ use crate::{
alerts::messages::UpdateShout,
server::actor::{ server::actor::{
messages::{ReceiveValidServerConfig, ValidServerConfig}, messages::{ReceiveValidServerConfig, ValidServerConfig},
ServerActor, ServerActor,
@ -18,7 +19,7 @@ use crate::{
impl Handler<ReceiveValidServerConfig> for ServerActor { impl Handler<ReceiveValidServerConfig> for ServerActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: ReceiveValidServerConfig, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveValidServerConfig, _ctx: &mut Self::Context) -> Self::Result {
let ValidServerConfig { let ValidServerConfig {
server_config, server_config,
socket_address, socket_address,
@ -33,6 +34,7 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
info!("Starting Webhook Server..."); info!("Starting Webhook Server...");
let webhook_router = WebhookRouter::default().start(); let webhook_router = WebhookRouter::default().start();
let listen_url = server_config.listen().url(); let listen_url = server_config.listen().url();
let alerts = self.alerts.clone();
// Forge Actors // Forge Actors
for (forge_alias, forge_config) in server_config.forges() { for (forge_alias, forge_config) in server_config.forges() {
let repo_actors = self let repo_actors = self
@ -41,7 +43,7 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
forge_alias.clone(), forge_alias.clone(),
&server_storage, &server_storage,
listen_url, listen_url,
ctx.address().recipient(), alerts.clone().recipient(),
) )
.into_iter() .into_iter()
.map(|a| self.start_actor(a)) .map(|a| self.start_actor(a))
@ -64,6 +66,8 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
let webhook_actor_addr = let webhook_actor_addr =
WebhookActor::new(socket_address, webhook_router.recipient()).start(); WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr); self.webhook_actor_addr.replace(webhook_actor_addr);
let shout = server_config.shout().clone();
self.server_config.replace(server_config); self.server_config.replace(server_config);
self.alerts.do_send(UpdateShout::new(shout));
} }
} }

View file

@ -10,11 +10,10 @@ mod handlers;
pub mod messages; pub mod messages;
use crate::{ use crate::{
alerts::messages::NotifyUser,
alerts::AlertsActor,
forge::Forge, forge::Forge,
repo::{ repo::{messages::CloneRepo, RepoActor},
messages::{CloneRepo, NotifyUser},
RepoActor,
},
webhook::WebhookActor, webhook::WebhookActor,
}; };
@ -56,6 +55,7 @@ pub struct ServerActor {
webhook_actor_addr: Option<Addr<WebhookActor>>, webhook_actor_addr: Option<Addr<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
net: Network, net: Network,
alerts: Addr<AlertsActor>,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>, repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
@ -71,6 +71,7 @@ impl ServerActor {
pub fn new( pub fn new(
fs: FileSystem, fs: FileSystem,
net: Network, net: Network,
alerts: Addr<AlertsActor>,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Self { ) -> Self {
@ -81,6 +82,7 @@ impl ServerActor {
webhook_actor_addr: None, webhook_actor_addr: None,
fs, fs,
net, net,
alerts,
repository_factory: repo, repository_factory: repo,
sleep_duration, sleep_duration,
repo_actors: BTreeMap::new(), repo_actors: BTreeMap::new(),

View file

@ -1,3 +1,8 @@
use actix::prelude::*;
use crate::alerts::{AlertsActor, History};
//
pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
} }
@ -5,3 +10,7 @@ pub fn a_filesystem() -> kxio::fs::FileSystem {
pub fn a_network() -> kxio::network::MockNetwork { pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new() kxio::network::MockNetwork::new()
} }
pub fn an_alerts_actor(net: kxio::network::Network) -> Addr<AlertsActor> {
AlertsActor::new(None, History::default(), net).start()
}

View file

@ -18,11 +18,12 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
// parameters // parameters
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let net = given::a_network(); let net = given::a_network();
let alerts = given::an_alerts_actor(net.clone().into());
let repo = git::repository::factory::mock(); let repo = git::repository::factory::mock();
let duration = std::time::Duration::from_millis(1); let duration = std::time::Duration::from_millis(1);
// sut // sut
let server = ServerActor::new(fs.clone(), net.into(), repo, duration); let server = ServerActor::new(fs.clone(), net.into(), alerts, repo, duration);
// collaborators // collaborators
let listen = Listen::new( let listen = Listen::new(

View file

@ -6,7 +6,10 @@ mod tests;
use actix::prelude::*; use actix::prelude::*;
use crate::file_watcher::{watch_file, FileUpdated}; use crate::{
alerts::{AlertsActor, History},
file_watcher::{watch_file, FileUpdated},
};
use actor::ServerActor; use actor::ServerActor;
use git_next_core::git::RepositoryFactory; use git_next_core::git::RepositoryFactory;
@ -41,8 +44,18 @@ pub fn start(
init_logging(); init_logging();
let execution = async move { let execution = async move {
info!("Starting Alert Dispatcher...");
let alerts_addr = AlertsActor::new(None, History::default(), net.clone()).start();
info!("Starting Server..."); info!("Starting Server...");
let server = ServerActor::new(fs.clone(), net.clone(), repo, sleep_duration).start(); let server = ServerActor::new(
fs.clone(),
net.clone(),
alerts_addr.clone(),
repo,
sleep_duration,
)
.start();
server.do_send(FileUpdated); server.do_send(FileUpdated);
info!("Starting File Watcher..."); info!("Starting File Watcher...");

View file

@ -8,6 +8,7 @@ url = "https://localhost:8080" # don't include any query path or a trailing slas
[shout] # where updates from git-next should be sent to alert the user [shout] # where updates from git-next should be sent to alert the user
# webhook = { url = "https//localhost:9090", secret = "secret-password" } # webhook = { url = "https//localhost:9090", secret = "secret-password" }
# desktop = true # enable desktop notifications
# [shout.email] # [shout.email]
# from = "git-next@example.com" # from = "git-next@example.com"

View file

@ -209,6 +209,7 @@ impl ServerStorage {
pub struct Shout { pub struct Shout {
webhook: Option<OutboundWebhook>, webhook: Option<OutboundWebhook>,
email: Option<EmailConfig>, email: Option<EmailConfig>,
desktop: bool,
} }
impl Shout { impl Shout {
pub const fn webhook(&self) -> Option<&OutboundWebhook> { pub const fn webhook(&self) -> Option<&OutboundWebhook> {
@ -226,6 +227,10 @@ impl Shout {
pub const fn email(&self) -> Option<&EmailConfig> { pub const fn email(&self) -> Option<&EmailConfig> {
self.email.as_ref() self.email.as_ref()
} }
pub const fn desktop(&self) -> bool {
self.desktop
}
} }
#[derive( #[derive(

View file

@ -527,6 +527,7 @@ url = "{listen_url}"
[shout] [shout]
webhook = {{ url = "{shout_webhook_url}", secret = "{shout_webhook_secret}" }} webhook = {{ url = "{shout_webhook_url}", secret = "{shout_webhook_secret}" }}
desktop = true
[shout.email] [shout.email]
from = "{shout_email_from}" from = "{shout_email_from}"
@ -773,7 +774,7 @@ mod given {
ServerStorage::new(a_name().into()) ServerStorage::new(a_name().into())
} }
pub fn a_shout() -> Shout { pub fn a_shout() -> Shout {
Shout::new(Some(an_outbound_webhook()), Some(an_email_config())) Shout::new(Some(an_outbound_webhook()), Some(an_email_config()), true)
} }
pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> { pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> {
[(a_name(), a_forge_config())].into() [(a_name(), a_forge_config())].into()