rustpbx 0.3.19

A SIP PBX implementation in Rust
Documentation
use sea_orm::entity::prelude::*;
use sea_orm::{ActiveValue::Set, ConnectionTrait, DbErr, QueryFilter};
use sea_orm_migration::prelude::*;
use sea_orm_migration::schema::{
    boolean, integer_null, string, string_null, text_null, timestamp, timestamp_null,
};
use sea_orm_migration::sea_query::ColumnDef;
use sea_query::Expr;
use serde::Serialize;

pub const DEFAULT_FORWARDING_TIMEOUT: i32 = 30;
pub const MIN_FORWARDING_TIMEOUT: i32 = 5;
pub const MAX_FORWARDING_TIMEOUT: i32 = 120;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Default)]
#[sea_orm(table_name = "rustpbx_extensions")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = true)]
    pub id: i64,
    #[sea_orm(unique)]
    pub extension: String,
    pub display_name: Option<String>,
    pub email: Option<String>,
    pub status: Option<String>,
    pub login_disabled: bool,
    pub voicemail_disabled: bool,
    pub allow_guest_calls: bool,
    pub sip_password: Option<String>,
    pub call_forwarding_mode: Option<String>,
    pub call_forwarding_destination: Option<String>,
    pub call_forwarding_timeout: Option<i32>,
    #[sea_orm(column_type = "DateTime")]
    pub registered_at: Option<DateTimeUtc>,
    pub notes: Option<String>,
    #[sea_orm(column_type = "DateTime", default_value = "CURRENT_TIMESTAMP")]
    pub created_at: DateTimeUtc,
    #[sea_orm(column_type = "DateTime", default_value = "CURRENT_TIMESTAMP")]
    pub updated_at: DateTimeUtc,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        panic!("no direct relations defined for extension");
    }
}

impl Related<super::department::Entity> for Entity {
    fn to() -> RelationDef {
        super::extension_department::Relation::Department.def()
    }

    fn via() -> Option<RelationDef> {
        Some(super::extension_department::Relation::Extension.def().rev())
    }
}

impl ActiveModelBehavior for ActiveModel {}

impl Entity {
    pub async fn find_by_id_with_departments<C>(
        conn: &C,
        id: i64,
    ) -> Result<Option<(Model, Vec<super::department::Model>)>, DbErr>
    where
        C: ConnectionTrait,
    {
        let mut results = Self::find()
            .filter(Column::Id.eq(id))
            .find_with_related(super::department::Entity)
            .all(conn)
            .await?;

        Ok(results.pop())
    }

    pub async fn replace_departments<C>(
        conn: &C,
        extension_id: i64,
        department_ids: &[i64],
    ) -> Result<(), DbErr>
    where
        C: ConnectionTrait,
    {
        super::extension_department::Entity::delete_many()
            .filter(super::extension_department::Column::ExtensionId.eq(extension_id))
            .exec(conn)
            .await?;

        if department_ids.is_empty() {
            return Ok(());
        }

        let models =
            department_ids
                .iter()
                .map(|department_id| super::extension_department::ActiveModel {
                    extension_id: Set(extension_id),
                    department_id: Set(*department_id),
                    ..Default::default()
                });

        super::extension_department::Entity::insert_many(models)
            .exec(conn)
            .await?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::{department, migration::Migrator};
    use sea_orm::Database;

    #[tokio::test]
    async fn extension_can_map_to_multiple_departments() {
        let db = Database::connect("sqlite::memory:")
            .await
            .expect("connect in-memory sqlite");

        Migrator::up(&db, None)
            .await
            .expect("migrations should succeed");

        let extension = ActiveModel {
            extension: Set("1001".to_string()),
            login_disabled: Set(false),
            voicemail_disabled: Set(false),
            allow_guest_calls: Set(false),
            call_forwarding_mode: Set(Some("none".to_string())),
            ..Default::default()
        }
        .insert(&db)
        .await
        .expect("insert extension");

        let sales = department::ActiveModel {
            name: Set("Sales".to_string()),
            ..Default::default()
        }
        .insert(&db)
        .await
        .expect("insert sales");

        let support = department::ActiveModel {
            name: Set("Support".to_string()),
            ..Default::default()
        }
        .insert(&db)
        .await
        .expect("insert support");

        Entity::replace_departments(&db, extension.id, &[sales.id, support.id])
            .await
            .expect("assign departments");

        let result = Entity::find_by_id_with_departments(&db, extension.id)
            .await
            .expect("query extension")
            .expect("extension exists");

        assert_eq!(result.1.len(), 2, "extension should have two departments");

        Entity::replace_departments(&db, extension.id, &[sales.id])
            .await
            .expect("reassign departments");

        let result = Entity::find_by_id_with_departments(&db, extension.id)
            .await
            .expect("query extension")
            .expect("extension exists");

        assert_eq!(result.1.len(), 1, "extension should have one department");
        assert_eq!(result.1[0].id, sales.id);
    }
}

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Entity)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Column::Id)
                            .big_integer()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(string(Column::Extension).char_len(32))
                    .col(string_null(Column::DisplayName).char_len(160))
                    .col(string_null(Column::Email).char_len(160))
                    .col(string_null(Column::Status).char_len(32))
                    .col(boolean(Column::LoginDisabled).default(false))
                    .col(boolean(Column::VoicemailDisabled).default(false))
                    .col(boolean(Column::AllowGuestCalls).default(false))
                    .col(string_null(Column::SipPassword).char_len(160))
                    .col(string_null(Column::CallForwardingMode).char_len(32))
                    .col(string_null(Column::CallForwardingDestination).char_len(160))
                    .col(integer_null(Column::CallForwardingTimeout))
                    .col(timestamp_null(Column::RegisteredAt))
                    .col(text_null(Column::Notes))
                    .col(timestamp(Column::CreatedAt).default(Expr::current_timestamp()))
                    .col(timestamp(Column::UpdatedAt).default(Expr::current_timestamp()))
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_rustpbx_extensions_extension")
                    .table(Entity)
                    .col(Column::Extension)
                    .unique()
                    .to_owned(),
            )
            .await?;
        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Entity).to_owned())
            .await
    }
}