shoutdotdev/src/messages.rs

122 lines
3.7 KiB
Rust
Raw Permalink Normal View History

2025-04-23 12:57:10 -07:00
use anyhow::Context as _;
use chrono::{DateTime, TimeDelta, Utc};
2025-02-26 13:10:47 -08:00
use diesel::{
dsl::{auto_type, AsSelect},
pg::Pg,
prelude::*,
};
use uuid::Uuid;
2025-04-23 12:57:10 -07:00
use crate::{
channels::Channel,
governors::{Governor, GovernorError},
projects::{self, Project},
schema::messages,
teams::{self, Team},
};
2025-02-26 13:10:47 -08:00
2025-04-21 19:02:23 -07:00
pub use crate::schema::messages::{dsl, table};
2025-04-23 12:57:10 -07:00
const TEAM_GOVERNOR_DEFAULT_WINDOW_SIZE_SEC: i64 = 300;
const TEAM_GOVERNOR_DEFAULT_MAX_COUNT: i32 = 50;
2025-03-14 13:04:57 -07:00
/// A "/say" message queued for sending
2025-02-26 13:10:47 -08:00
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = messages)]
#[diesel(belongs_to(Channel))]
2025-02-26 13:10:47 -08:00
pub struct Message {
pub id: Uuid,
2025-03-08 22:18:24 -08:00
pub project_id: Uuid,
pub channel_id: Uuid,
2025-02-26 13:10:47 -08:00
pub created_at: DateTime<Utc>,
2025-03-08 22:18:24 -08:00
pub sent_at: Option<DateTime<Utc>>,
2025-04-21 19:02:23 -07:00
pub failed_at: Option<DateTime<Utc>>,
2025-02-26 13:10:47 -08:00
pub message: String,
}
impl Message {
#[auto_type(no_type_alias)]
pub fn all() -> _ {
let select: AsSelect<Message, Pg> = Message::as_select();
2025-04-21 19:02:23 -07:00
table.select(select)
}
#[auto_type(no_type_alias)]
pub fn with_id<'a>(id: &'a Uuid) -> _ {
dsl::id.eq(id)
2025-02-26 13:10:47 -08:00
}
#[auto_type(no_type_alias)]
2025-03-14 13:04:57 -07:00
pub fn with_channel<'a>(channel_id: &'a Uuid) -> _ {
2025-04-21 19:02:23 -07:00
dsl::channel_id.eq(channel_id)
2025-02-26 13:10:47 -08:00
}
2025-03-08 22:18:24 -08:00
#[auto_type(no_type_alias)]
2025-04-21 19:02:23 -07:00
pub fn is_pending() -> _ {
dsl::sent_at.is_null().and(dsl::failed_at.is_null())
2025-03-08 22:18:24 -08:00
}
2025-02-26 13:10:47 -08:00
}
2025-04-23 12:57:10 -07:00
#[derive(Clone, Debug, Insertable)]
#[diesel(table_name = messages)]
struct InsertableMessage<'a> {
id: Uuid,
project_id: Uuid,
channel_id: Uuid,
message: &'a str,
}
#[derive(Clone, Debug)]
pub struct Broadcast {
pub project_id: Uuid,
pub message: String,
}
impl Broadcast {
pub fn queue_messages(&self, db_conn: &mut PgConnection) -> Result<Vec<Uuid>, GovernorError> {
let _guard = tracing::debug_span!(
"Broadcast::queue_messages()",
project = self.project_id.hyphenated().to_string()
)
.entered();
let (project, team): (Project, Team) = projects::table
.inner_join(teams::table)
.filter(Project::with_id(&self.project_id))
.first(db_conn)
.context("failed to load project and team")?;
let selected_channels = project
.selected_channels()
.load(db_conn)
.context("failed to load selected channels")?;
Governor::lazy_getter()
.with_team_id(team.id)
.with_default_window_size(TimeDelta::seconds(TEAM_GOVERNOR_DEFAULT_WINDOW_SIZE_SEC))
.with_default_max_count(TEAM_GOVERNOR_DEFAULT_MAX_COUNT)
.build()
.context("failed to build governor lazy getter")?
.load(db_conn)
.context("failed to lazily get governor")?
.create_entries(selected_channels.len(), db_conn)?;
let mut message_ids: Vec<Uuid> = Vec::with_capacity(selected_channels.len());
for channel in selected_channels {
message_ids.push(
diesel::insert_into(messages::table)
.values(InsertableMessage {
id: Uuid::now_v7(),
project_id: project.id,
channel_id: channel.id,
message: &self.message,
})
.returning(dsl::id)
.get_result(db_conn)
.context("failed to queue message")
.inspect_err(|_| {
tracing::error!("error queuing message for channel {}", channel.id);
})?,
);
}
tracing::debug!("queued {} messages", message_ids.len());
Ok(message_ids)
}
}