177 lines
5.5 KiB
Rust
177 lines
5.5 KiB
Rust
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, Pg> = Channel::as_select();
|
|
channels::table.select(select)
|
|
}
|
|
|
|
#[auto_type(no_type_alias)]
|
|
pub fn with_id<'a>(channel_id: &'a Uuid) -> _ {
|
|
channels::id.eq(channel_id)
|
|
}
|
|
|
|
#[auto_type(no_type_alias)]
|
|
pub fn with_team<'a>(team_id: &'a Uuid) -> _ {
|
|
channels::team_id.eq(team_id)
|
|
}
|
|
|
|
#[auto_type(no_type_alias)]
|
|
pub fn where_enabled_by_default() -> _ {
|
|
channels::enable_by_default.eq(true)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<Jsonb, Pg> for BackendConfig {
|
|
fn to_sql<'a>(&self, out: &mut Output<'a, '_, Pg>) -> diesel::serialize::Result {
|
|
match self.clone() {
|
|
BackendConfig::Email(config) => ToSql::<Jsonb, Pg>::to_sql(
|
|
&json!({
|
|
"t": CHANNEL_BACKEND_EMAIL,
|
|
"config": config,
|
|
}),
|
|
&mut out.reborrow(),
|
|
),
|
|
BackendConfig::Slack(config) => ToSql::<Jsonb, Pg>::to_sql(
|
|
&json!({
|
|
"t": CHANNEL_BACKEND_SLACK,
|
|
"config": config,
|
|
}),
|
|
&mut out.reborrow(),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromSql<Jsonb, Pg> for BackendConfig {
|
|
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
|
|
let value = <serde_json::Value as FromSql<Jsonb, Pg>>::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<BackendConfig> for EmailBackendConfig {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: BackendConfig) -> Result<Self, Self::Error> {
|
|
if let BackendConfig::Email(config) = value {
|
|
Ok(config)
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"could not load backend config as email channel"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Into<BackendConfig> 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<BackendConfig> for SlackBackendConfig {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: BackendConfig) -> Result<Self, Self::Error> {
|
|
if let BackendConfig::Slack(config) = value {
|
|
Ok(config)
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"could not load backend config as slack channel"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Into<BackendConfig> for SlackBackendConfig {
|
|
fn into(self) -> BackendConfig {
|
|
BackendConfig::Slack(self)
|
|
}
|
|
}
|