1
0
Fork 0
forked from 2sys/shoutdotdev
shoutdotdev/src/channels.rs

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)
}
}