use diesel::{ backend::Backend, deserialize::{self, FromSql, FromSqlRow}, dsl::{auto_type, AsSelect}, expression::AsExpression, pg::Pg, prelude::*, serialize::{Output, ToSql}, sql_types::Jsonb, }; use serde::{Deserialize, Serialize}; use serde_json::json; use uuid::Uuid; use crate::{schema::channels, teams::Team}; pub const CHANNEL_BACKEND_EMAIL: &'static str = "email"; pub const CHANNEL_BACKEND_SLACK: &'static str = "slack"; /** * Represents a target/destination for messages, with the sender configuration * defined in the backend_config field. A single channel may be attached to * (in other words, "enabled" or "selected" for) any number of projects within * the same team. */ #[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)] #[diesel(belongs_to(Team))] #[diesel(check_for_backend(Pg))] pub struct Channel { pub id: Uuid, pub team_id: Uuid, pub name: String, pub enable_by_default: bool, pub backend_config: BackendConfig, } impl Channel { #[auto_type(no_type_alias)] pub fn all() -> _ { let select: AsSelect = Channel::as_select(); channels::table.select(select) } #[auto_type(no_type_alias)] pub fn with_id(channel_id: Uuid) -> _ { channels::id.eq(channel_id) } #[auto_type(no_type_alias)] pub fn with_team(team_id: Uuid) -> _ { channels::team_id.eq(team_id) } } /** * Encapsulates any information that needs to be persisted for setting up or * using a channel's backend (that is, email sender, Slack app, etc.). This * configuration is encoded to a jsonb column in the database, which determines * the channel type along with configuration details. * * Note: In a previous implementation, channel configuration was handled by * creating a dedicated table for each channel type and joining them to the * `channels` table in order to access configuration fields. The jsonb approach * simplifies database management and lends itself to a cleaner Rust * implementation in which this enum can be treated as a column type with * enforcement of data structure invariants handled entirely in the to_sql() * and from_sql() serialization/deserialization logic. */ #[derive(AsExpression, Clone, Debug, FromSqlRow, Deserialize, Serialize)] #[diesel(sql_type = Jsonb)] pub enum BackendConfig { Email(EmailBackendConfig), Slack(SlackBackendConfig), } impl ToSql for BackendConfig { fn to_sql<'a>(&self, out: &mut Output<'a, '_, Pg>) -> diesel::serialize::Result { match self.clone() { BackendConfig::Email(config) => ToSql::::to_sql( &json!({ "t": CHANNEL_BACKEND_EMAIL, "config": config, }), &mut out.reborrow(), ), BackendConfig::Slack(config) => ToSql::::to_sql( &json!({ "t": CHANNEL_BACKEND_SLACK, "config": config, }), &mut out.reborrow(), ), } } } impl FromSql for BackendConfig { fn from_sql(bytes: ::RawValue<'_>) -> deserialize::Result { let value = >::from_sql(bytes)?; #[derive(Deserialize)] struct IntermediateBackendConfig { pub t: String, pub config: serde_json::Value, } let intermediate_value = IntermediateBackendConfig::deserialize(value)?; match intermediate_value.t.as_str() { CHANNEL_BACKEND_EMAIL => Ok(BackendConfig::Email(EmailBackendConfig::deserialize( intermediate_value.config, )?)), CHANNEL_BACKEND_SLACK => Ok(BackendConfig::Slack(SlackBackendConfig::deserialize( intermediate_value.config, )?)), t => Err(anyhow::anyhow!("backend type not recognized: {}", t).into()), } } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct EmailBackendConfig { pub recipient: String, pub verification_code: String, pub verification_code_guesses: u32, pub verified: bool, } impl TryFrom for EmailBackendConfig { type Error = anyhow::Error; fn try_from(value: BackendConfig) -> Result { if let BackendConfig::Email(config) = value { Ok(config) } else { Err(anyhow::anyhow!( "could not load backend config as email channel" )) } } } impl Into for EmailBackendConfig { fn into(self) -> BackendConfig { BackendConfig::Email(self) } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SlackBackendConfig { pub oauth_state: String, pub access_token: String, pub refresh_token: String, pub conversation_id: String, } impl TryFrom for SlackBackendConfig { type Error = anyhow::Error; fn try_from(value: BackendConfig) -> Result { if let BackendConfig::Slack(config) = value { Ok(config) } else { Err(anyhow::anyhow!( "could not load backend config as slack channel" )) } } } impl Into for SlackBackendConfig { fn into(self) -> BackendConfig { BackendConfig::Slack(self) } }