Compare commits
No commits in common. "f3bc16d701292c5e4e4d48a65e32b5fd2572a060" and "421e85cb0b56081d09ca680ee085b75801d18786" have entirely different histories.
f3bc16d701
...
421e85cb0b
18 changed files with 163 additions and 239 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -945,6 +945,8 @@ dependencies = [
|
|||
"actix-rt",
|
||||
"anyhow",
|
||||
"assert2",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"clap",
|
||||
"derive-with",
|
||||
|
@ -962,6 +964,7 @@ dependencies = [
|
|||
"rand",
|
||||
"secrecy",
|
||||
"sendmail",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"standardwebhooks",
|
||||
"test-log",
|
||||
|
@ -979,6 +982,7 @@ name = "git-next-core"
|
|||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-rt",
|
||||
"assert2",
|
||||
"async-trait",
|
||||
"derive-with",
|
||||
|
@ -995,9 +999,9 @@ dependencies = [
|
|||
"serde_json",
|
||||
"test-log",
|
||||
"thiserror",
|
||||
"time",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
|
@ -1007,6 +1011,9 @@ version = "0.13.0"
|
|||
dependencies = [
|
||||
"assert2",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"git-next-core",
|
||||
"kxio",
|
||||
"rand",
|
||||
|
@ -1014,7 +1021,9 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1023,6 +1032,8 @@ version = "0.13.0"
|
|||
dependencies = [
|
||||
"assert2",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"git-next-core",
|
||||
|
@ -1035,7 +1046,9 @@ dependencies = [
|
|||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -31,12 +31,18 @@ kxio = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
toml = { workspace = true }
|
||||
|
||||
# base64 decoding
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
|
@ -48,6 +54,7 @@ anyhow = { workspace = true }
|
|||
thiserror = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
time = { workspace = true }
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
//
|
||||
use crate::alerts::short_message;
|
||||
use git_next_core::git::UserNotification;
|
||||
use crate::alerts::{messages::NotifyUser, short_message};
|
||||
|
||||
pub(super) fn send_desktop_notification(user_notification: &UserNotification) {
|
||||
let message = short_message(user_notification);
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
//
|
||||
use git_next_core::{
|
||||
git::UserNotification,
|
||||
server::{EmailConfig, SmtpConfig},
|
||||
};
|
||||
use git_next_core::server::{EmailConfig, SmtpConfig};
|
||||
|
||||
use crate::alerts::{full_message, short_message};
|
||||
use crate::alerts::{full_message, messages::NotifyUser, short_message};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EmailMessage {
|
||||
|
@ -14,12 +11,12 @@ struct EmailMessage {
|
|||
body: String,
|
||||
}
|
||||
|
||||
pub(super) fn send_email(user_notification: &UserNotification, email_config: &EmailConfig) {
|
||||
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(user_notification),
|
||||
body: full_message(user_notification),
|
||||
subject: short_message(msg),
|
||||
body: full_message(msg),
|
||||
};
|
||||
match email_config.smtp() {
|
||||
Some(smtp) => send_email_smtp(email_message, smtp),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{info, instrument, Instrument as _};
|
||||
|
||||
use crate::alerts::{
|
||||
|
@ -19,16 +18,15 @@ impl Handler<NotifyUser> for AlertsActor {
|
|||
};
|
||||
let net = self.net.clone();
|
||||
let shout = shout.clone();
|
||||
if let Some(user_notification) = self.history.sendable(msg.unwrap()) {
|
||||
async move {
|
||||
if let Some(webhook_config) = shout.webhook() {
|
||||
send_webhook(&user_notification, webhook_config, &net).await;
|
||||
send_webhook(&msg, webhook_config, &net).await;
|
||||
}
|
||||
if let Some(email_config) = shout.email() {
|
||||
send_email(&user_notification, email_config);
|
||||
send_email(&msg, email_config);
|
||||
}
|
||||
if shout.desktop() {
|
||||
send_desktop_notification(&user_notification);
|
||||
send_desktop_notification(&msg);
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
|
@ -36,4 +34,3 @@ impl Handler<NotifyUser> for AlertsActor {
|
|||
.wait(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,2 @@
|
|||
//
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct History {
|
||||
/// The maximum age of an item in the history.
|
||||
///
|
||||
/// Items older than this will be dropped.
|
||||
max_age_seconds: Duration,
|
||||
|
||||
/// Maps a user notification to when it was last seen.
|
||||
///
|
||||
/// The user notification will not be sent until after max_age_seconds from last seen.
|
||||
///
|
||||
/// Each time we see a given user notification, the last seen time will be updated.
|
||||
items: HashMap<UserNotification, Instant>,
|
||||
}
|
||||
impl History {
|
||||
pub fn new(max_age_seconds: Duration) -> Self {
|
||||
Self {
|
||||
max_age_seconds,
|
||||
items: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendable(&mut self, user_notification: UserNotification) -> Option<UserNotification> {
|
||||
let now = Instant::now();
|
||||
self.prune(&now); // remove expired items first
|
||||
let contains_key = self.items.contains_key(&user_notification);
|
||||
self.items.insert(user_notification.clone(), now);
|
||||
if contains_key {
|
||||
return None;
|
||||
}
|
||||
Some(user_notification)
|
||||
}
|
||||
|
||||
pub fn prune(&mut self, now: &Instant) {
|
||||
if let Some(threshold) = now.checked_sub(self.max_age_seconds) {
|
||||
self.items.retain(|_, last_seen| *last_seen > threshold)
|
||||
};
|
||||
}
|
||||
}
|
||||
pub struct History {}
|
||||
|
|
|
@ -1,6 +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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::ops::Deref as _;
|
||||
|
||||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
|
@ -6,6 +8,7 @@ use derive_more::derive::Constructor;
|
|||
use git_next_core::{git::UserNotification, server::Shout};
|
||||
|
||||
pub use history::History;
|
||||
use messages::NotifyUser;
|
||||
|
||||
mod desktop;
|
||||
mod email;
|
||||
|
@ -14,12 +17,10 @@ mod history;
|
|||
pub mod messages;
|
||||
mod webhook;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
@ -28,8 +29,8 @@ impl Actor for AlertsActor {
|
|||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
fn short_message(user_notification: &UserNotification) -> String {
|
||||
let tail = match user_notification {
|
||||
fn short_message(msg: &NotifyUser) -> String {
|
||||
let tail = match msg.deref() {
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
|
@ -57,8 +58,8 @@ fn short_message(user_notification: &UserNotification) -> String {
|
|||
format!("[git-next] {tail}")
|
||||
}
|
||||
|
||||
fn full_message(user_notification: &UserNotification) -> String {
|
||||
match user_notification {
|
||||
fn full_message(msg: &NotifyUser) -> String {
|
||||
match msg.deref() {
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use assert2::let_assert;
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use crate::{alerts::History, repo::tests::given};
|
||||
|
||||
#[test]
|
||||
fn when_history_is_empty_then_message_is_passed() {
|
||||
let mut history = History::new(Duration::from_millis(1));
|
||||
|
||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
||||
forge_alias: given::a_forge_alias(),
|
||||
repo_alias: given::a_repo_alias(),
|
||||
reason: given::a_name(),
|
||||
};
|
||||
|
||||
let result = history.sendable(user_notification);
|
||||
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_history_has_expired_then_message_is_passed() {
|
||||
let dur = Duration::from_millis(1);
|
||||
let mut history = History::new(dur);
|
||||
|
||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
||||
forge_alias: given::a_forge_alias(),
|
||||
repo_alias: given::a_repo_alias(),
|
||||
reason: given::a_name(),
|
||||
};
|
||||
|
||||
// add message to history
|
||||
let result = history.sendable(user_notification);
|
||||
let_assert!(Some(user_notification) = result);
|
||||
|
||||
std::thread::sleep(dur);
|
||||
|
||||
// after dur, message has expired, so is still valid
|
||||
let result = history.sendable(user_notification);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_history_has_unexpired_then_message_is_blocked() {
|
||||
let dur = Duration::from_millis(1);
|
||||
let mut history = History::new(dur);
|
||||
|
||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
||||
forge_alias: given::a_forge_alias(),
|
||||
repo_alias: given::a_repo_alias(),
|
||||
reason: given::a_name(),
|
||||
};
|
||||
|
||||
// add message to history
|
||||
let result = history.sendable(user_notification);
|
||||
let_assert!(Some(user_notification) = result);
|
||||
|
||||
// no time passed
|
||||
// std::thread::sleep(dur);
|
||||
|
||||
// after dur, message has expired, so is still valid
|
||||
let result = history.sendable(user_notification);
|
||||
assert!(result.is_none());
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
mod history;
|
|
@ -1,10 +1,12 @@
|
|||
//
|
||||
use git_next_core::{git::UserNotification, server::OutboundWebhook};
|
||||
use git_next_core::server::OutboundWebhook;
|
||||
use secrecy::ExposeSecret as _;
|
||||
use standardwebhooks::Webhook;
|
||||
|
||||
use crate::alerts::messages::NotifyUser;
|
||||
|
||||
pub(super) async fn send_webhook(
|
||||
user_notification: &UserNotification,
|
||||
msg: &NotifyUser,
|
||||
webhook_config: &OutboundWebhook,
|
||||
net: &kxio::network::Network,
|
||||
) {
|
||||
|
@ -14,18 +16,18 @@ pub(super) async fn send_webhook(
|
|||
tracing::warn!("Invalid notification configuration (signer) - can't sent notification");
|
||||
return;
|
||||
};
|
||||
do_send_webhook(user_notification, webhook, webhook_config, net).await;
|
||||
do_send_webhook(msg, webhook, webhook_config, net).await;
|
||||
}
|
||||
|
||||
async fn do_send_webhook(
|
||||
user_notification: &UserNotification,
|
||||
msg: &NotifyUser,
|
||||
webhook: Webhook,
|
||||
webhook_config: &OutboundWebhook,
|
||||
net: &kxio::network::Network,
|
||||
) {
|
||||
let message_id = format!("msg_{}", ulid::Ulid::new());
|
||||
let timestamp = time::OffsetDateTime::now_utc();
|
||||
let payload = user_notification.as_json(timestamp);
|
||||
let payload = msg.as_json(timestamp);
|
||||
let timestamp = timestamp.unix_timestamp();
|
||||
let to_sign = format!("{message_id}.{timestamp}.{payload}");
|
||||
tracing::info!(?to_sign, "");
|
||||
|
|
|
@ -24,7 +24,7 @@ pub mod messages;
|
|||
mod notifications;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RepoActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
|
||||
|
|
|
@ -14,12 +14,14 @@ github = []
|
|||
[dependencies]
|
||||
# logging
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
|
@ -35,7 +37,6 @@ async-trait = { workspace = true }
|
|||
|
||||
# Webhooks
|
||||
ulid = { workspace = true }
|
||||
time = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use derive_more::derive::Display;
|
||||
use serde::Serialize;
|
||||
|
||||
crate::newtype!(BranchName: String, Display, Default, Hash, Serialize: "The name of a Git branch");
|
||||
crate::newtype!(BranchName: String, derive_more::Display, Default, Serialize: "The name of a Git branch");
|
||||
|
|
|
@ -3,6 +3,6 @@ use serde::Serialize;
|
|||
|
||||
use crate::newtype;
|
||||
|
||||
newtype!(RepoAlias: String, Display, Default, Hash, PartialOrd, Ord, Serialize: r#"The alias of a repo.
|
||||
newtype!(RepoAlias: String, Display, Default, PartialOrd, Ord, Serialize: r#"The alias of a repo.
|
||||
|
||||
This is the alias for the repo within `git-next-server.toml`."#);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
//
|
||||
use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum UserNotification {
|
||||
CICheckFailed {
|
||||
forge_alias: ForgeAlias,
|
||||
|
@ -28,81 +27,3 @@ pub enum UserNotification {
|
|||
main_commit: Commit,
|
||||
},
|
||||
}
|
||||
impl UserNotification {
|
||||
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
||||
let timestamp = timestamp.unix_timestamp().to_string();
|
||||
match self {
|
||||
Self::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()
|
||||
}
|
||||
}
|
||||
}),
|
||||
Self::RepoConfigLoadFailure {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
reason,
|
||||
} => json!({
|
||||
"type": "config.load.failed",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"reason": reason
|
||||
}
|
||||
}),
|
||||
Self::WebhookRegistration {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
reason,
|
||||
} => json!({
|
||||
"type": "webhook.registration.failed",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"reason": reason
|
||||
}
|
||||
}),
|
||||
Self::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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ git-next-core = { workspace = true }
|
|||
# logging
|
||||
tracing = { workspace = true }
|
||||
|
||||
# base64 decoding
|
||||
base64 = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
|
@ -21,10 +24,18 @@ kxio = { workspace = true }
|
|||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
bytes = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
||||
# # Actors
|
||||
tokio = { workspace = true }
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ hmac = { workspace = true }
|
|||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
|
||||
# base64 decoding
|
||||
base64 = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
|
@ -29,10 +32,15 @@ kxio = { workspace = true }
|
|||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
bytes = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
||||
|
|
Loading…
Reference in a new issue