fix: Multiple compilation fixes per PROMPT.md
Video module: - Fix state.db -> state.conn field name - Fix analytics.rs imports placement - Remove AppState dependency from websocket.rs (use global broadcaster) - Simplify render.rs broadcaster usage Other modules: - Add sha1 crate dependency - Fix AppState import paths (project, legal) - Fix db_pool -> conn throughout codebase - Add missing types: RefundResult, ExternalSyncError, TasksIntegrationError, RecordingError, FallbackAttemptTracker - Add stub implementations for GoogleContactsClient, MicrosoftPeopleClient - Fix social/mod.rs format string - Fix designer/canvas.rs SVG path - Remove doc comments per PROMPT.md - Add missing handler implementations in calendar_integration.rs
This commit is contained in:
parent
998e4c2806
commit
a4cbf145d2
26 changed files with 532 additions and 437 deletions
|
|
@ -137,6 +137,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
sha2 = "0.10.9"
|
||||
sha1 = "0.10.6"
|
||||
tokio = { version = "1.41", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tower = "0.4"
|
||||
|
|
|
|||
|
|
@ -59,6 +59,16 @@ pub struct InvoiceDiscount {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RefundResult {
|
||||
pub id: Uuid,
|
||||
pub invoice_id: Uuid,
|
||||
pub amount: i64,
|
||||
pub currency: String,
|
||||
pub reason: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct InvoiceTax {
|
||||
pub id: Uuid,
|
||||
pub description: String,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
//! Contacts-Calendar Integration Module
|
||||
//!
|
||||
//! This module provides integration between the Contacts and Calendar apps,
|
||||
//! allowing contacts to be linked to calendar events and providing contact
|
||||
//! context for meetings.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::IntoResponse,
|
||||
|
|
@ -19,7 +13,6 @@ use uuid::Uuid;
|
|||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
/// A contact linked to a calendar event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventContact {
|
||||
pub id: Uuid,
|
||||
|
|
@ -32,7 +25,6 @@ pub struct EventContact {
|
|||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Role of a contact in an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum EventContactRole {
|
||||
#[default]
|
||||
|
|
@ -57,7 +49,6 @@ impl std::fmt::Display for EventContactRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// Response status for event invitation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum ResponseStatus {
|
||||
#[default]
|
||||
|
|
@ -80,7 +71,6 @@ impl std::fmt::Display for ResponseStatus {
|
|||
}
|
||||
}
|
||||
|
||||
/// Request to link a contact to an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkContactRequest {
|
||||
pub contact_id: Uuid,
|
||||
|
|
@ -88,7 +78,6 @@ pub struct LinkContactRequest {
|
|||
pub send_notification: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request to link multiple contacts to an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BulkLinkContactsRequest {
|
||||
pub contact_ids: Vec<Uuid>,
|
||||
|
|
@ -96,14 +85,12 @@ pub struct BulkLinkContactsRequest {
|
|||
pub send_notification: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request to update a contact's role or status in an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateEventContactRequest {
|
||||
pub role: Option<EventContactRole>,
|
||||
pub response_status: Option<ResponseStatus>,
|
||||
}
|
||||
|
||||
/// Query parameters for listing event contacts
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventContactsQuery {
|
||||
pub role: Option<EventContactRole>,
|
||||
|
|
@ -111,7 +98,6 @@ pub struct EventContactsQuery {
|
|||
pub include_contact_details: Option<bool>,
|
||||
}
|
||||
|
||||
/// Query parameters for listing contact's events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactEventsQuery {
|
||||
pub from_date: Option<DateTime<Utc>>,
|
||||
|
|
@ -122,14 +108,12 @@ pub struct ContactEventsQuery {
|
|||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
/// Event contact with full contact details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventContactWithDetails {
|
||||
pub event_contact: EventContact,
|
||||
pub contact: ContactSummary,
|
||||
}
|
||||
|
||||
/// Summary of contact information for display
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactSummary {
|
||||
pub id: Uuid,
|
||||
|
|
@ -142,7 +126,6 @@ pub struct ContactSummary {
|
|||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Event summary for contact view
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventSummary {
|
||||
pub id: Uuid,
|
||||
|
|
@ -155,14 +138,12 @@ pub struct EventSummary {
|
|||
pub organizer_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Contact's event with role information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactEventWithDetails {
|
||||
pub event_contact: EventContact,
|
||||
pub event: EventSummary,
|
||||
}
|
||||
|
||||
/// Response for listing contact events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactEventsResponse {
|
||||
pub events: Vec<ContactEventWithDetails>,
|
||||
|
|
@ -171,7 +152,6 @@ pub struct ContactEventsResponse {
|
|||
pub past_count: u32,
|
||||
}
|
||||
|
||||
/// Suggested contacts based on event context
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SuggestedContact {
|
||||
pub contact: ContactSummary,
|
||||
|
|
@ -179,7 +159,6 @@ pub struct SuggestedContact {
|
|||
pub score: f32,
|
||||
}
|
||||
|
||||
/// Reason for contact suggestion
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum SuggestionReason {
|
||||
FrequentCollaborator,
|
||||
|
|
@ -203,7 +182,6 @@ impl std::fmt::Display for SuggestionReason {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calendar integration service for contacts
|
||||
pub struct CalendarIntegrationService {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
|
@ -213,7 +191,6 @@ impl CalendarIntegrationService {
|
|||
Self { pool }
|
||||
}
|
||||
|
||||
/// Link a contact to a calendar event
|
||||
pub async fn link_contact_to_event(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -267,7 +244,6 @@ impl CalendarIntegrationService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Link multiple contacts to an event
|
||||
pub async fn bulk_link_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -296,7 +272,6 @@ impl CalendarIntegrationService {
|
|||
Ok(results)
|
||||
}
|
||||
|
||||
/// Unlink a contact from an event
|
||||
pub async fn unlink_contact_from_event(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -322,7 +297,6 @@ impl CalendarIntegrationService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a contact's role or status in an event
|
||||
pub async fn update_event_contact(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -348,7 +322,6 @@ impl CalendarIntegrationService {
|
|||
Ok(event_contact)
|
||||
}
|
||||
|
||||
/// Get all contacts linked to an event
|
||||
pub async fn get_event_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -390,7 +363,6 @@ impl CalendarIntegrationService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get all events for a contact
|
||||
pub async fn get_contact_events(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -417,7 +389,6 @@ impl CalendarIntegrationService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Get suggested contacts for an event
|
||||
pub async fn get_suggested_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -480,7 +451,6 @@ impl CalendarIntegrationService {
|
|||
Ok(suggestions)
|
||||
}
|
||||
|
||||
/// Find contacts by email for quick add to event
|
||||
pub async fn find_contacts_for_event(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -496,7 +466,6 @@ impl CalendarIntegrationService {
|
|||
Ok(results)
|
||||
}
|
||||
|
||||
/// Create contacts from event attendees who don't exist
|
||||
pub async fn create_contacts_from_attendees(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -763,7 +732,6 @@ impl CalendarIntegrationService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Attendee information for creating contacts
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AttendeeInfo {
|
||||
pub email: String,
|
||||
|
|
@ -771,7 +739,6 @@ pub struct AttendeeInfo {
|
|||
pub company: Option<String>,
|
||||
}
|
||||
|
||||
/// Error types for calendar integration
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CalendarIntegrationError {
|
||||
DatabaseError,
|
||||
|
|
@ -825,7 +792,6 @@ impl IntoResponse for CalendarIntegrationError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create database tables migration
|
||||
pub fn create_calendar_integration_tables_migration() -> String {
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS event_contacts (
|
||||
|
|
@ -849,7 +815,6 @@ pub fn create_calendar_integration_tables_migration() -> String {
|
|||
.to_string()
|
||||
}
|
||||
|
||||
/// API routes for calendar integration
|
||||
pub fn calendar_integration_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// Event contacts
|
||||
|
|
@ -888,8 +853,8 @@ async fn get_event_contacts_handler(
|
|||
Path(event_id): Path<Uuid>,
|
||||
Query(query): Query<EventContactsQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.db_pool.clone());
|
||||
let org_id = Uuid::new_v4(); // Get from auth context
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.get_event_contacts(org_id, event_id, &query).await {
|
||||
Ok(contacts) => Json(contacts).into_response(),
|
||||
|
|
@ -902,11 +867,106 @@ async fn link_contact_handler(
|
|||
Path(event_id): Path<Uuid>,
|
||||
Json(request): Json<LinkContactRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.db_pool.clone());
|
||||
let org_id = Uuid::new_v4(); // Get from auth context
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.link_contact_to_event(org_id, event_id, &request).await {
|
||||
Ok(event_contact) => Json(event_contact).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn bulk_link_contacts_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
Json(request): Json<BulkLinkContactsRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.bulk_link_contacts(org_id, event_id, &request).await {
|
||||
Ok(contacts) => Json(contacts).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn unlink_contact_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.unlink_contact_from_event(org_id, event_id, contact_id).await {
|
||||
Ok(_) => Json(serde_json::json!({ "success": true })).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_event_contact_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
||||
Json(request): Json<UpdateEventContactRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.update_event_contact(org_id, event_id, contact_id, &request).await {
|
||||
Ok(contact) => Json(contact).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_suggestions_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.get_suggested_contacts(org_id, event_id).await {
|
||||
Ok(suggestions) => Json(suggestions).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_contact_events_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(contact_id): Path<Uuid>,
|
||||
Query(query): Query<ContactEventsQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.get_contact_events(org_id, contact_id, &query).await {
|
||||
Ok(events) => Json(events).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_contacts_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.find_contacts_for_event(org_id, event_id).await {
|
||||
Ok(contacts) => Json(contacts).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_contacts_from_attendees_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
Json(attendees): Json<Vec<AttendeeInfo>>,
|
||||
) -> impl IntoResponse {
|
||||
let service = CalendarIntegrationService::new(state.conn.clone());
|
||||
let org_id = Uuid::new_v4();
|
||||
|
||||
match service.create_contacts_from_attendees(org_id, event_id, &attendees).await {
|
||||
Ok(contacts) => Json(contacts).into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
//! External Address Book Synchronization Module
|
||||
//!
|
||||
//! This module provides synchronization between the internal Contacts app
|
||||
//! and external address book providers like Google Contacts and Microsoft
|
||||
//! People (Outlook/Office 365).
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::IntoResponse,
|
||||
|
|
@ -19,7 +13,151 @@ use uuid::Uuid;
|
|||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
/// Supported external providers
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GoogleConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MicrosoftConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub tenant_id: String,
|
||||
}
|
||||
|
||||
pub struct GoogleContactsClient {
|
||||
config: GoogleConfig,
|
||||
}
|
||||
|
||||
impl GoogleContactsClient {
|
||||
pub fn new(config: GoogleConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String {
|
||||
format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&state={}&scope=https://www.googleapis.com/auth/contacts&response_type=code",
|
||||
self.config.client_id, redirect_uri, state
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn exchange_code(&self, _code: &str, _redirect_uri: &str) -> Result<TokenResponse, ExternalSyncError> {
|
||||
Ok(TokenResponse {
|
||||
access_token: String::new(),
|
||||
refresh_token: Some(String::new()),
|
||||
expires_in: 3600,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_contacts(&self, _access_token: &str) -> Result<Vec<ExternalContact>, ExternalSyncError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn create_contact(&self, _access_token: &str, _contact: &ExternalContact) -> Result<String, ExternalSyncError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
pub async fn update_contact(&self, _access_token: &str, _external_id: &str, _contact: &ExternalContact) -> Result<(), ExternalSyncError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_contact(&self, _access_token: &str, _external_id: &str) -> Result<(), ExternalSyncError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MicrosoftPeopleClient {
|
||||
config: MicrosoftConfig,
|
||||
}
|
||||
|
||||
impl MicrosoftPeopleClient {
|
||||
pub fn new(config: MicrosoftConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String {
|
||||
format!(
|
||||
"https://login.microsoftonline.com/{}/oauth2/v2.0/authorize?client_id={}&redirect_uri={}&state={}&scope=Contacts.ReadWrite&response_type=code",
|
||||
self.config.tenant_id, self.config.client_id, redirect_uri, state
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn exchange_code(&self, _code: &str, _redirect_uri: &str) -> Result<TokenResponse, ExternalSyncError> {
|
||||
Ok(TokenResponse {
|
||||
access_token: String::new(),
|
||||
refresh_token: Some(String::new()),
|
||||
expires_in: 3600,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_contacts(&self, _access_token: &str) -> Result<Vec<ExternalContact>, ExternalSyncError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn create_contact(&self, _access_token: &str, _contact: &ExternalContact) -> Result<String, ExternalSyncError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
pub async fn update_contact(&self, _access_token: &str, _external_id: &str, _contact: &ExternalContact) -> Result<(), ExternalSyncError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_contact(&self, _access_token: &str, _external_id: &str) -> Result<(), ExternalSyncError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokenResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: Option<String>,
|
||||
pub expires_in: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ImportResult {
|
||||
Created,
|
||||
Updated,
|
||||
Skipped,
|
||||
Conflict,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ExportResult {
|
||||
Created,
|
||||
Updated,
|
||||
Deleted,
|
||||
Skipped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExternalSyncError {
|
||||
DatabaseError(String),
|
||||
UnsupportedProvider(String),
|
||||
Unauthorized,
|
||||
SyncDisabled,
|
||||
SyncInProgress,
|
||||
ApiError(String),
|
||||
InvalidData(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExternalSyncError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||
Self::UnsupportedProvider(p) => write!(f, "Unsupported provider: {p}"),
|
||||
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||
Self::SyncDisabled => write!(f, "Sync is disabled"),
|
||||
Self::SyncInProgress => write!(f, "Sync already in progress"),
|
||||
Self::ApiError(e) => write!(f, "API error: {e}"),
|
||||
Self::InvalidData(e) => write!(f, "Invalid data: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ExternalSyncError {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum ExternalProvider {
|
||||
Google,
|
||||
|
|
@ -40,7 +178,7 @@ impl std::fmt::Display for ExternalProvider {
|
|||
}
|
||||
|
||||
impl std::str::FromStr for ExternalProvider {
|
||||
type Err = ExternalSyncError;
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
|
|
@ -48,12 +186,11 @@ impl std::str::FromStr for ExternalProvider {
|
|||
"microsoft" => Ok(ExternalProvider::Microsoft),
|
||||
"apple" => Ok(ExternalProvider::Apple),
|
||||
"carddav" => Ok(ExternalProvider::CardDav),
|
||||
_ => Err(ExternalSyncError::UnsupportedProvider(s.to_string())),
|
||||
_ => Err(format!("Unsupported provider: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// External account connection
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalAccount {
|
||||
pub id: Uuid,
|
||||
|
|
@ -76,7 +213,6 @@ pub struct ExternalAccount {
|
|||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Sync direction configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum SyncDirection {
|
||||
#[default]
|
||||
|
|
@ -95,7 +231,6 @@ impl std::fmt::Display for SyncDirection {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sync operation status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SyncStatus {
|
||||
Success,
|
||||
|
|
@ -117,7 +252,6 @@ impl std::fmt::Display for SyncStatus {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mapping between internal and external contact
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactMapping {
|
||||
pub id: Uuid,
|
||||
|
|
@ -131,7 +265,6 @@ pub struct ContactMapping {
|
|||
pub conflict_data: Option<ConflictData>,
|
||||
}
|
||||
|
||||
/// Sync status for individual contact mapping
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum MappingSyncStatus {
|
||||
Synced,
|
||||
|
|
@ -155,7 +288,6 @@ impl std::fmt::Display for MappingSyncStatus {
|
|||
}
|
||||
}
|
||||
|
||||
/// Conflict information when sync encounters conflicting changes
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConflictData {
|
||||
pub detected_at: DateTime<Utc>,
|
||||
|
|
@ -165,7 +297,6 @@ pub struct ConflictData {
|
|||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// How to resolve a sync conflict
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ConflictResolution {
|
||||
KeepInternal,
|
||||
|
|
@ -174,7 +305,6 @@ pub enum ConflictResolution {
|
|||
Skip,
|
||||
}
|
||||
|
||||
/// Sync history record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncHistory {
|
||||
pub id: Uuid,
|
||||
|
|
@ -192,7 +322,6 @@ pub struct SyncHistory {
|
|||
pub triggered_by: SyncTrigger,
|
||||
}
|
||||
|
||||
/// What triggered the sync
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum SyncTrigger {
|
||||
Manual,
|
||||
|
|
@ -212,7 +341,6 @@ impl std::fmt::Display for SyncTrigger {
|
|||
}
|
||||
}
|
||||
|
||||
/// Individual sync error
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncError {
|
||||
pub contact_id: Option<Uuid>,
|
||||
|
|
@ -223,7 +351,6 @@ pub struct SyncError {
|
|||
pub retryable: bool,
|
||||
}
|
||||
|
||||
/// Request to connect an external account
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectAccountRequest {
|
||||
pub provider: ExternalProvider,
|
||||
|
|
@ -232,21 +359,18 @@ pub struct ConnectAccountRequest {
|
|||
pub sync_direction: Option<SyncDirection>,
|
||||
}
|
||||
|
||||
/// Response with OAuth authorization URL
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthorizationUrlResponse {
|
||||
pub url: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Request to start manual sync
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StartSyncRequest {
|
||||
pub full_sync: Option<bool>,
|
||||
pub direction: Option<SyncDirection>,
|
||||
}
|
||||
|
||||
/// Sync progress response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncProgressResponse {
|
||||
pub sync_id: Uuid,
|
||||
|
|
@ -259,14 +383,12 @@ pub struct SyncProgressResponse {
|
|||
pub estimated_completion: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Request to resolve a conflict
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolveConflictRequest {
|
||||
pub resolution: ConflictResolution,
|
||||
pub merged_data: Option<MergedContactData>,
|
||||
}
|
||||
|
||||
/// Merged contact data for manual conflict resolution
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MergedContactData {
|
||||
pub first_name: Option<String>,
|
||||
|
|
@ -278,7 +400,6 @@ pub struct MergedContactData {
|
|||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Sync settings for an account
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncSettings {
|
||||
pub sync_enabled: bool,
|
||||
|
|
@ -308,7 +429,6 @@ impl Default for SyncSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// Account status response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AccountStatusResponse {
|
||||
pub account: ExternalAccount,
|
||||
|
|
@ -318,7 +438,6 @@ pub struct AccountStatusResponse {
|
|||
pub next_scheduled_sync: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Sync statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncStats {
|
||||
pub total_synced_contacts: u32,
|
||||
|
|
@ -329,7 +448,6 @@ pub struct SyncStats {
|
|||
pub average_sync_duration_seconds: u32,
|
||||
}
|
||||
|
||||
/// External contact representation (provider-agnostic)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalContact {
|
||||
pub id: String,
|
||||
|
|
@ -377,7 +495,6 @@ pub struct ExternalAddress {
|
|||
pub primary: bool,
|
||||
}
|
||||
|
||||
/// External sync service
|
||||
pub struct ExternalSyncService {
|
||||
pool: DbPool,
|
||||
google_client: GoogleContactsClient,
|
||||
|
|
@ -393,7 +510,6 @@ impl ExternalSyncService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get OAuth authorization URL for a provider
|
||||
pub fn get_authorization_url(
|
||||
&self,
|
||||
provider: &ExternalProvider,
|
||||
|
|
@ -419,7 +535,6 @@ impl ExternalSyncService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Connect an external account using OAuth authorization code
|
||||
pub async fn connect_account(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -499,7 +614,6 @@ impl ExternalSyncService {
|
|||
Ok(account)
|
||||
}
|
||||
|
||||
/// Disconnect an external account
|
||||
pub async fn disconnect_account(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -531,7 +645,6 @@ impl ExternalSyncService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a sync operation
|
||||
pub async fn start_sync(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -617,7 +730,6 @@ impl ExternalSyncService {
|
|||
Ok(history)
|
||||
}
|
||||
|
||||
/// Perform two-way sync
|
||||
async fn perform_two_way_sync(
|
||||
&self,
|
||||
account: &ExternalAccount,
|
||||
|
|
@ -633,7 +745,6 @@ impl ExternalSyncService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Import contacts from external provider
|
||||
async fn perform_import_sync(
|
||||
&self,
|
||||
account: &ExternalAccount,
|
||||
|
|
@ -692,7 +803,6 @@ impl ExternalSyncService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Export contacts to external provider
|
||||
async fn perform_export_sync(
|
||||
&self,
|
||||
account: &ExternalAccount,
|
||||
|
|
@ -723,7 +833,6 @@ impl ExternalSyncService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Import a single contact
|
||||
async fn import_contact(
|
||||
&self,
|
||||
account: &ExternalAccount,
|
||||
|
|
@ -778,7 +887,6 @@ impl ExternalSyncService {
|
|||
Ok(ImportResult::Created)
|
||||
}
|
||||
|
||||
/// Export a single contact
|
||||
async fn export_contact(
|
||||
&self,
|
||||
account: &ExternalAccount,
|
||||
|
|
@ -841,7 +949,6 @@ impl ExternalSyncService {
|
|||
Ok(ExportResult::Updated)
|
||||
}
|
||||
|
||||
/// Get list of connected accounts
|
||||
pub async fn list_accounts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -868,7 +975,6 @@ impl ExternalSyncService {
|
|||
Ok(results)
|
||||
}
|
||||
|
||||
/// Get sync history for an account
|
||||
pub async fn get_sync_history(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -884,7 +990,6 @@ impl ExternalSyncService {
|
|||
self.fetch_sync_history(account_id, limit.unwrap_or(20)).await
|
||||
}
|
||||
|
||||
/// Get pending conflicts for an account
|
||||
pub async fn get_conflicts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -899,7 +1004,6 @@ impl ExternalSyncService {
|
|||
self.fetch_conflicts(account_id).await
|
||||
}
|
||||
|
||||
/// Resolve a sync conflict
|
||||
pub async fn resolve_conflict(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
|
|||
|
|
@ -1334,7 +1334,7 @@ async fn list_contacts_handler(
|
|||
Query(query): Query<ContactListQuery>,
|
||||
organization_id: Uuid,
|
||||
) -> Result<Json<ContactListResponse>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let response = service.list_contacts(organization_id, query).await?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
|
@ -1345,7 +1345,7 @@ async fn create_contact_handler(
|
|||
user_id: Option<Uuid>,
|
||||
Json(request): Json<CreateContactRequest>,
|
||||
) -> Result<Json<Contact>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let contact = service.create_contact(organization_id, user_id, request).await?;
|
||||
Ok(Json(contact))
|
||||
}
|
||||
|
|
@ -1355,7 +1355,7 @@ async fn get_contact_handler(
|
|||
Path(contact_id): Path<Uuid>,
|
||||
organization_id: Uuid,
|
||||
) -> Result<Json<Contact>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let contact = service.get_contact(organization_id, contact_id).await?;
|
||||
Ok(Json(contact))
|
||||
}
|
||||
|
|
@ -1367,7 +1367,7 @@ async fn update_contact_handler(
|
|||
user_id: Option<Uuid>,
|
||||
Json(request): Json<UpdateContactRequest>,
|
||||
) -> Result<Json<Contact>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let contact = service.update_contact(organization_id, contact_id, request, user_id).await?;
|
||||
Ok(Json(contact))
|
||||
}
|
||||
|
|
@ -1377,7 +1377,7 @@ async fn delete_contact_handler(
|
|||
Path(contact_id): Path<Uuid>,
|
||||
organization_id: Uuid,
|
||||
) -> Result<StatusCode, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
service.delete_contact(organization_id, contact_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
|
@ -1388,7 +1388,7 @@ async fn import_contacts_handler(
|
|||
user_id: Option<Uuid>,
|
||||
Json(request): Json<ImportRequest>,
|
||||
) -> Result<Json<ImportResult>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let result = service.import_contacts(organization_id, user_id, request).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
|
@ -1398,7 +1398,7 @@ async fn export_contacts_handler(
|
|||
organization_id: Uuid,
|
||||
Json(request): Json<ExportRequest>,
|
||||
) -> Result<Json<ExportResult>, ContactsError> {
|
||||
let service = ContactsService::new(state.db_pool.clone());
|
||||
let service = ContactsService::new(state.conn.clone());
|
||||
let result = service.export_contacts(organization_id, request).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
//! Contacts-Tasks Integration Module
|
||||
//!
|
||||
//! This module provides integration between the Contacts and Tasks apps,
|
||||
//! allowing contacts to be assigned to tasks and providing contact
|
||||
//! context for task management.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::IntoResponse,
|
||||
|
|
@ -19,7 +13,47 @@ use uuid::Uuid;
|
|||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
/// A contact assigned to a task
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TasksIntegrationError {
|
||||
DatabaseError(String),
|
||||
ContactNotFound,
|
||||
TaskNotFound,
|
||||
AlreadyAssigned,
|
||||
NotAssigned,
|
||||
Unauthorized,
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TasksIntegrationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||
Self::ContactNotFound => write!(f, "Contact not found"),
|
||||
Self::TaskNotFound => write!(f, "Task not found"),
|
||||
Self::AlreadyAssigned => write!(f, "Contact already assigned"),
|
||||
Self::NotAssigned => write!(f, "Contact not assigned"),
|
||||
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||
Self::InvalidInput(e) => write!(f, "Invalid input: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TasksIntegrationError {}
|
||||
|
||||
impl IntoResponse for TasksIntegrationError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::http::StatusCode;
|
||||
let status = match &self {
|
||||
Self::ContactNotFound | Self::TaskNotFound => StatusCode::NOT_FOUND,
|
||||
Self::AlreadyAssigned | Self::NotAssigned => StatusCode::CONFLICT,
|
||||
Self::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Self::InvalidInput(_) => StatusCode::BAD_REQUEST,
|
||||
Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(serde_json::json!({ "error": self.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskContact {
|
||||
pub id: Uuid,
|
||||
|
|
@ -33,8 +67,7 @@ pub struct TaskContact {
|
|||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Role of a contact in a task
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub enum TaskContactRole {
|
||||
#[default]
|
||||
Assignee,
|
||||
|
|
@ -62,7 +95,6 @@ impl std::fmt::Display for TaskContactRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// Request to assign a contact to a task
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AssignContactRequest {
|
||||
pub contact_id: Uuid,
|
||||
|
|
@ -71,14 +103,12 @@ pub struct AssignContactRequest {
|
|||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Request to assign multiple contacts to a task
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BulkAssignContactsRequest {
|
||||
pub assignments: Vec<ContactAssignment>,
|
||||
pub send_notification: Option<bool>,
|
||||
}
|
||||
|
||||
/// Individual contact assignment
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactAssignment {
|
||||
pub contact_id: Uuid,
|
||||
|
|
@ -86,21 +116,18 @@ pub struct ContactAssignment {
|
|||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Request to update a contact's assignment
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateTaskContactRequest {
|
||||
pub role: Option<TaskContactRole>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Query parameters for listing task contacts
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskContactsQuery {
|
||||
pub role: Option<TaskContactRole>,
|
||||
pub include_contact_details: Option<bool>,
|
||||
}
|
||||
|
||||
/// Query parameters for listing contact's tasks
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactTasksQuery {
|
||||
pub status: Option<String>,
|
||||
|
|
@ -115,7 +142,6 @@ pub struct ContactTasksQuery {
|
|||
pub sort_order: Option<SortOrder>,
|
||||
}
|
||||
|
||||
/// Sort fields for tasks
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum TaskSortField {
|
||||
#[default]
|
||||
|
|
@ -126,7 +152,6 @@ pub enum TaskSortField {
|
|||
Title,
|
||||
}
|
||||
|
||||
/// Sort order
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum SortOrder {
|
||||
#[default]
|
||||
|
|
@ -134,14 +159,12 @@ pub enum SortOrder {
|
|||
Desc,
|
||||
}
|
||||
|
||||
/// Task contact with full contact details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskContactWithDetails {
|
||||
pub task_contact: TaskContact,
|
||||
pub contact: ContactSummary,
|
||||
}
|
||||
|
||||
/// Summary of contact information for display
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactSummary {
|
||||
pub id: Uuid,
|
||||
|
|
@ -160,7 +183,6 @@ impl ContactSummary {
|
|||
}
|
||||
}
|
||||
|
||||
/// Task summary for contact view
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskSummary {
|
||||
pub id: Uuid,
|
||||
|
|
@ -176,14 +198,12 @@ pub struct TaskSummary {
|
|||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Contact's task with role information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactTaskWithDetails {
|
||||
pub task_contact: TaskContact,
|
||||
pub task: TaskSummary,
|
||||
}
|
||||
|
||||
/// Response for listing contact tasks
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactTasksResponse {
|
||||
pub tasks: Vec<ContactTaskWithDetails>,
|
||||
|
|
@ -195,7 +215,6 @@ pub struct ContactTasksResponse {
|
|||
pub due_this_week_count: u32,
|
||||
}
|
||||
|
||||
/// Task statistics for a contact
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactTaskStats {
|
||||
pub contact_id: Uuid,
|
||||
|
|
@ -209,7 +228,6 @@ pub struct ContactTaskStats {
|
|||
pub recent_activity: Vec<TaskActivity>,
|
||||
}
|
||||
|
||||
/// Task activity record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskActivity {
|
||||
pub id: Uuid,
|
||||
|
|
@ -220,7 +238,6 @@ pub struct TaskActivity {
|
|||
pub occurred_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Types of task activities
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TaskActivityType {
|
||||
Assigned,
|
||||
|
|
@ -246,7 +263,6 @@ impl std::fmt::Display for TaskActivityType {
|
|||
}
|
||||
}
|
||||
|
||||
/// Suggested contacts for task assignment
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SuggestedTaskContact {
|
||||
pub contact: ContactSummary,
|
||||
|
|
@ -255,7 +271,6 @@ pub struct SuggestedTaskContact {
|
|||
pub workload: ContactWorkload,
|
||||
}
|
||||
|
||||
/// Reason for suggesting a contact for a task
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TaskSuggestionReason {
|
||||
PreviouslyAssigned,
|
||||
|
|
@ -281,7 +296,6 @@ impl std::fmt::Display for TaskSuggestionReason {
|
|||
}
|
||||
}
|
||||
|
||||
/// Contact's current workload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactWorkload {
|
||||
pub active_tasks: u32,
|
||||
|
|
@ -291,7 +305,6 @@ pub struct ContactWorkload {
|
|||
pub workload_level: WorkloadLevel,
|
||||
}
|
||||
|
||||
/// Workload level indicator
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum WorkloadLevel {
|
||||
Low,
|
||||
|
|
@ -311,7 +324,6 @@ impl std::fmt::Display for WorkloadLevel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Request to create a task and assign to contact
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateTaskForContactRequest {
|
||||
pub title: String,
|
||||
|
|
@ -324,7 +336,6 @@ pub struct CreateTaskForContactRequest {
|
|||
pub send_notification: Option<bool>,
|
||||
}
|
||||
|
||||
/// Tasks integration service for contacts
|
||||
pub struct TasksIntegrationService {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
|
@ -334,7 +345,6 @@ impl TasksIntegrationService {
|
|||
Self { pool }
|
||||
}
|
||||
|
||||
/// Assign a contact to a task
|
||||
pub async fn assign_contact_to_task(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -400,7 +410,6 @@ impl TasksIntegrationService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Assign multiple contacts to a task
|
||||
pub async fn bulk_assign_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -431,7 +440,6 @@ impl TasksIntegrationService {
|
|||
Ok(results)
|
||||
}
|
||||
|
||||
/// Unassign a contact from a task
|
||||
pub async fn unassign_contact_from_task(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -454,7 +462,6 @@ impl TasksIntegrationService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a contact's assignment
|
||||
pub async fn update_task_contact(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -480,7 +487,6 @@ impl TasksIntegrationService {
|
|||
Ok(task_contact)
|
||||
}
|
||||
|
||||
/// Get all contacts assigned to a task
|
||||
pub async fn get_task_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -522,7 +528,6 @@ impl TasksIntegrationService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get all tasks for a contact
|
||||
pub async fn get_contact_tasks(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -569,7 +574,6 @@ impl TasksIntegrationService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Get task statistics for a contact
|
||||
pub async fn get_contact_task_stats(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -582,7 +586,6 @@ impl TasksIntegrationService {
|
|||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Get suggested contacts for a task
|
||||
pub async fn get_suggested_contacts(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -650,7 +653,6 @@ impl TasksIntegrationService {
|
|||
Ok(suggestions)
|
||||
}
|
||||
|
||||
/// Get contact's workload
|
||||
pub async fn get_contact_workload(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
@ -663,7 +665,6 @@ impl TasksIntegrationService {
|
|||
Ok(workload)
|
||||
}
|
||||
|
||||
/// Create a task and assign to contact in one operation
|
||||
pub async fn create_task_for_contact(
|
||||
&self,
|
||||
organization_id: Uuid,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl KbContextManager {
|
|||
}
|
||||
|
||||
pub fn get_active_kbs(&self, session_id: Uuid) -> Result<Vec<SessionKbAssociation>> {
|
||||
let mut conn = self.db_pool.get()?;
|
||||
let mut conn = self.conn.get()?;
|
||||
|
||||
let query = diesel::sql_query(
|
||||
"SELECT kb_name, qdrant_collection, kb_folder_path, is_active
|
||||
|
|
@ -227,7 +227,7 @@ impl KbContextManager {
|
|||
}
|
||||
|
||||
pub fn get_active_tools(&self, session_id: Uuid) -> Result<Vec<String>> {
|
||||
let mut conn = self.db_pool.get()?;
|
||||
let mut conn = self.conn.get()?;
|
||||
|
||||
let query = diesel::sql_query(
|
||||
"SELECT tool_name
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ impl UserProvisioningService {
|
|||
.to_string();
|
||||
|
||||
let mut conn = self
|
||||
.db_pool
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||
diesel::insert_into(users::table)
|
||||
|
|
@ -184,7 +184,7 @@ impl UserProvisioningService {
|
|||
use diesel::prelude::*;
|
||||
|
||||
let mut conn = self
|
||||
.db_pool
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ impl UserProvisioningService {
|
|||
];
|
||||
|
||||
let mut conn = self
|
||||
.db_pool
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||
for (key, value) in services {
|
||||
|
|
@ -259,7 +259,7 @@ impl UserProvisioningService {
|
|||
use diesel::prelude::*;
|
||||
|
||||
let mut conn = self
|
||||
.db_pool
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||
diesel::delete(users::table.filter(users::username.eq(username))).execute(&mut conn)?;
|
||||
|
|
@ -310,7 +310,7 @@ impl UserProvisioningService {
|
|||
use diesel::prelude::*;
|
||||
|
||||
let mut conn = self
|
||||
.db_pool
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
|
||||
diesel::delete(
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ impl WebsiteCrawlerService {
|
|||
fn check_and_crawl_websites(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("Checking for websites that need recrawling");
|
||||
|
||||
let mut conn = self.db_pool.get()?;
|
||||
let mut conn = self.conn.get()?;
|
||||
|
||||
let websites = diesel::sql_query(
|
||||
"SELECT id, bot_id, url, expires_policy, max_depth, max_pages
|
||||
|
|
@ -77,7 +77,7 @@ impl WebsiteCrawlerService {
|
|||
.execute(&mut conn)?;
|
||||
|
||||
let kb_manager = Arc::clone(&self.kb_manager);
|
||||
let db_pool = self.db_pool.clone();
|
||||
let db_pool = self.conn.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::crawl_website(website, kb_manager, db_pool).await {
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ impl ContextMiddlewareState {
|
|||
"#,
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_optional(self.db_pool.as_ref())
|
||||
.fetch_optional(self.conn.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
|
@ -395,7 +395,7 @@ impl ContextMiddlewareState {
|
|||
)
|
||||
.bind(user_id)
|
||||
.bind(org_id)
|
||||
.fetch_all(self.db_pool.as_ref())
|
||||
.fetch_all(self.conn.as_ref())
|
||||
.await;
|
||||
|
||||
if let Ok(r) = role_result {
|
||||
|
|
@ -413,7 +413,7 @@ impl ContextMiddlewareState {
|
|||
)
|
||||
.bind(user_id)
|
||||
.bind(org_id)
|
||||
.fetch_all(self.db_pool.as_ref())
|
||||
.fetch_all(self.conn.as_ref())
|
||||
.await;
|
||||
|
||||
if let Ok(g) = group_result {
|
||||
|
|
@ -459,18 +459,13 @@ impl ContextMiddlewareState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
#[derive(Debug)]
|
||||
struct OrganizationRow {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
plan_id: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Middleware Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Extract organization context from request and add to extensions
|
||||
pub async fn organization_context_middleware(
|
||||
State(state): State<Arc<ContextMiddlewareState>>,
|
||||
mut request: Request<Body>,
|
||||
|
|
|
|||
|
|
@ -1334,7 +1334,7 @@ impl CanvasService {
|
|||
|
||||
pub async fn get_asset_library(&self, asset_type: Option<AssetType>) -> Result<Vec<AssetLibraryItem>, CanvasError> {
|
||||
let icons = vec![
|
||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Bot".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some(include_str!("../../botui/ui/suite/assets/icons/gb-bot.svg").to_string()), category: "General Bots".to_string(), tags: vec!["bot".to_string(), "assistant".to_string()], is_system: true },
|
||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Bot".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some(include_str!("../../../botui/ui/suite/assets/icons/gb-bot.svg").to_string()), category: "General Bots".to_string(), tags: vec!["bot".to_string(), "assistant".to_string()], is_system: true },
|
||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Analytics".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["analytics".to_string(), "chart".to_string()], is_system: true },
|
||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Calendar".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["calendar".to_string(), "date".to_string()], is_system: true },
|
||||
AssetLibraryItem { id: Uuid::new_v4(), name: "Chat".to_string(), asset_type: AssetType::Icon, url: None, svg_content: Some("<svg></svg>".to_string()), category: "General Bots".to_string(), tags: vec!["chat".to_string(), "message".to_string()], is_system: true },
|
||||
|
|
@ -1455,7 +1455,7 @@ async fn create_canvas_handler(
|
|||
user_id: Uuid,
|
||||
Json(request): Json<CreateCanvasRequest>,
|
||||
) -> Result<Json<Canvas>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let canvas = service.create_canvas(organization_id, user_id, request).await?;
|
||||
Ok(Json(canvas))
|
||||
}
|
||||
|
|
@ -1464,7 +1464,7 @@ async fn get_canvas_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(canvas_id): Path<Uuid>,
|
||||
) -> Result<Json<Canvas>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let canvas = service.get_canvas(canvas_id).await?;
|
||||
Ok(Json(canvas))
|
||||
}
|
||||
|
|
@ -1475,7 +1475,7 @@ async fn add_element_handler(
|
|||
user_id: Uuid,
|
||||
Json(request): Json<AddElementRequest>,
|
||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let element = service.add_element(canvas_id, user_id, request).await?;
|
||||
Ok(Json(element))
|
||||
}
|
||||
|
|
@ -1486,7 +1486,7 @@ async fn update_element_handler(
|
|||
user_id: Uuid,
|
||||
Json(request): Json<UpdateElementRequest>,
|
||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let element = service.update_element(canvas_id, element_id, user_id, request).await?;
|
||||
Ok(Json(element))
|
||||
}
|
||||
|
|
@ -1496,7 +1496,7 @@ async fn delete_element_handler(
|
|||
Path((canvas_id, element_id)): Path<(Uuid, Uuid)>,
|
||||
user_id: Uuid,
|
||||
) -> Result<StatusCode, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
service.delete_element(canvas_id, element_id, user_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
|
@ -1507,7 +1507,7 @@ async fn group_elements_handler(
|
|||
user_id: Uuid,
|
||||
Json(request): Json<GroupElementsRequest>,
|
||||
) -> Result<Json<CanvasElement>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let group = service.group_elements(canvas_id, user_id, request).await?;
|
||||
Ok(Json(group))
|
||||
}
|
||||
|
|
@ -1518,7 +1518,7 @@ async fn add_layer_handler(
|
|||
user_id: Uuid,
|
||||
Json(request): Json<CreateLayerRequest>,
|
||||
) -> Result<Json<Layer>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let layer = service.add_layer(canvas_id, user_id, request).await?;
|
||||
Ok(Json(layer))
|
||||
}
|
||||
|
|
@ -1528,7 +1528,7 @@ async fn export_canvas_handler(
|
|||
Path(canvas_id): Path<Uuid>,
|
||||
Json(request): Json<ExportRequest>,
|
||||
) -> Result<Json<ExportResult>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let result = service.export_canvas(canvas_id, request).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
|
@ -1542,7 +1542,7 @@ async fn get_templates_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<TemplatesQuery>,
|
||||
) -> Result<Json<Vec<CanvasTemplate>>, CanvasError> {
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let templates = service.get_templates(query.category).await?;
|
||||
Ok(Json(templates))
|
||||
}
|
||||
|
|
@ -1565,7 +1565,7 @@ async fn get_assets_handler(
|
|||
_ => None,
|
||||
});
|
||||
|
||||
let service = CanvasService::new(state.db_pool.clone());
|
||||
let service = CanvasService::new(state.conn.clone());
|
||||
let assets = service.get_asset_library(asset_type).await?;
|
||||
Ok(Json(assets))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1606,7 +1606,7 @@ pub async fn list_courses(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(filters): Query<CourseFilters>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.list_courses(filters).await {
|
||||
Ok(courses) => Json(serde_json::json!({
|
||||
|
|
@ -1630,7 +1630,7 @@ pub async fn create_course(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateCourseRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.create_course(req, None, None).await {
|
||||
Ok(course) => (
|
||||
|
|
@ -1657,7 +1657,7 @@ pub async fn get_course(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(course_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_course(course_id).await {
|
||||
Ok(Some(course)) => {
|
||||
|
|
@ -1699,7 +1699,7 @@ pub async fn update_course(
|
|||
Path(course_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateCourseRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.update_course(course_id, req).await {
|
||||
Ok(course) => Json(serde_json::json!({
|
||||
|
|
@ -1723,7 +1723,7 @@ pub async fn delete_course(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(course_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.delete_course(course_id).await {
|
||||
Ok(()) => Json(serde_json::json!({
|
||||
|
|
@ -1747,7 +1747,7 @@ pub async fn get_lessons(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(course_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_lessons(course_id).await {
|
||||
Ok(lessons) => Json(serde_json::json!({
|
||||
|
|
@ -1772,7 +1772,7 @@ pub async fn create_lesson(
|
|||
Path(course_id): Path<Uuid>,
|
||||
Json(req): Json<CreateLessonRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.create_lesson(course_id, req).await {
|
||||
Ok(lesson) => (
|
||||
|
|
@ -1800,7 +1800,7 @@ pub async fn update_lesson(
|
|||
Path(lesson_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateLessonRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.update_lesson(lesson_id, req).await {
|
||||
Ok(lesson) => Json(serde_json::json!({
|
||||
|
|
@ -1824,7 +1824,7 @@ pub async fn delete_lesson(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.delete_lesson(lesson_id).await {
|
||||
Ok(()) => Json(serde_json::json!({
|
||||
|
|
@ -1848,7 +1848,7 @@ pub async fn get_quiz(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(course_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_quiz(course_id).await {
|
||||
Ok(Some(quiz)) => Json(serde_json::json!({
|
||||
|
|
@ -1881,7 +1881,7 @@ pub async fn submit_quiz(
|
|||
Path(course_id): Path<Uuid>,
|
||||
Json(submission): Json<QuizSubmission>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// Get quiz ID first
|
||||
let quiz = match engine.get_quiz(course_id).await {
|
||||
|
|
@ -1933,7 +1933,7 @@ pub async fn get_progress(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(filters): Query<ProgressFilters>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -1960,7 +1960,7 @@ pub async fn start_course(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(course_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -1987,7 +1987,7 @@ pub async fn complete_lesson_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -2014,7 +2014,7 @@ pub async fn create_assignment(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateAssignmentRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get assigner user_id from session
|
||||
let assigned_by = None;
|
||||
|
|
@ -2041,7 +2041,7 @@ pub async fn create_assignment(
|
|||
|
||||
/// Get pending assignments
|
||||
pub async fn get_pending_assignments(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -2068,7 +2068,7 @@ pub async fn delete_assignment(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(assignment_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.delete_assignment(assignment_id).await {
|
||||
Ok(()) => Json(serde_json::json!({
|
||||
|
|
@ -2089,7 +2089,7 @@ pub async fn delete_assignment(
|
|||
|
||||
/// Get user certificates
|
||||
pub async fn get_certificates(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -2126,7 +2126,7 @@ pub async fn verify_certificate(Path(code): Path<String>) -> impl IntoResponse {
|
|||
|
||||
/// Get categories
|
||||
pub async fn get_categories(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_categories().await {
|
||||
Ok(categories) => Json(serde_json::json!({
|
||||
|
|
@ -2147,7 +2147,7 @@ pub async fn get_categories(State(state): State<Arc<AppState>>) -> impl IntoResp
|
|||
|
||||
/// Get AI recommendations
|
||||
pub async fn get_recommendations(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
@ -2171,7 +2171,7 @@ pub async fn get_recommendations(State(state): State<Arc<AppState>>) -> impl Int
|
|||
|
||||
/// Get learn statistics
|
||||
pub async fn get_statistics(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_statistics().await {
|
||||
Ok(stats) => Json(serde_json::json!({
|
||||
|
|
@ -2192,7 +2192,7 @@ pub async fn get_statistics(State(state): State<Arc<AppState>>) -> impl IntoResp
|
|||
|
||||
/// Get user stats
|
||||
pub async fn get_user_stats(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let engine = LearnEngine::new(state.db_pool.clone());
|
||||
let engine = LearnEngine::new(state.conn.clone());
|
||||
|
||||
// TODO: Get user_id from session
|
||||
let user_id = Uuid::new_v4();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::sync::Arc;
|
|||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ impl std::fmt::Debug for CachedLLMProvider {
|
|||
.field("cache", &self.cache)
|
||||
.field("config", &self.config)
|
||||
.field("embedding_service", &self.embedding_service.is_some())
|
||||
.field("db_pool", &self.db_pool.is_some())
|
||||
.field("db_pool", &self.conn.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ impl CachedLLMProvider {
|
|||
}
|
||||
|
||||
async fn is_cache_enabled(&self, bot_id: &str) -> bool {
|
||||
if let Some(ref db_pool) = self.db_pool {
|
||||
if let Some(ref db_pool) = self.conn {
|
||||
let bot_uuid = match Uuid::parse_str(bot_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
|
|
@ -181,7 +181,7 @@ impl CachedLLMProvider {
|
|||
}
|
||||
|
||||
fn get_bot_cache_config(&self, bot_id: &str) -> CacheConfig {
|
||||
if let Some(ref db_pool) = self.db_pool {
|
||||
if let Some(ref db_pool) = self.conn {
|
||||
let bot_uuid = match Uuid::parse_str(bot_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
|
|
|
|||
|
|
@ -1012,7 +1012,7 @@ async fn preview_cleanup_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<PreviewQuery>,
|
||||
) -> Result<Json<CleanupPreview>, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
|
||||
let categories = query.categories.map(|s| {
|
||||
s.split(',')
|
||||
|
|
@ -1041,7 +1041,7 @@ async fn execute_cleanup_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<ExecuteRequest>,
|
||||
) -> Result<Json<CleanupResult>, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
|
||||
let categories = request.categories.map(|cats| {
|
||||
cats.iter()
|
||||
|
|
@ -1076,7 +1076,7 @@ async fn storage_usage_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<StorageQuery>,
|
||||
) -> Result<Json<StorageUsage>, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
let usage = service.get_storage_usage(query.organization_id).await?;
|
||||
Ok(Json(usage))
|
||||
}
|
||||
|
|
@ -1085,7 +1085,7 @@ async fn cleanup_history_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<HistoryQuery>,
|
||||
) -> Result<Json<Vec<CleanupHistory>>, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
let history = service
|
||||
.get_cleanup_history(query.organization_id, query.limit)
|
||||
.await?;
|
||||
|
|
@ -1096,7 +1096,7 @@ async fn get_config_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<StorageQuery>,
|
||||
) -> Result<Json<CleanupConfig>, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
let config = service.get_cleanup_config(query.organization_id).await?;
|
||||
Ok(Json(config))
|
||||
}
|
||||
|
|
@ -1105,7 +1105,7 @@ async fn save_config_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(config): Json<CleanupConfig>,
|
||||
) -> Result<StatusCode, CleanupError> {
|
||||
let service = CleanupService::new(state.db_pool.clone());
|
||||
let service = CleanupService::new(state.conn.clone());
|
||||
service.save_cleanup_config(&config).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
//! Webinar Recording and Transcription Service
|
||||
//!
|
||||
//! This module provides recording and automatic transcription capabilities for webinars.
|
||||
//! It integrates with various transcription providers and handles the full lifecycle
|
||||
//! of recordings from capture to processing to storage.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
|
|
@ -20,45 +14,59 @@ use uuid::Uuid;
|
|||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RecordingError {
|
||||
DatabaseError(String),
|
||||
NotFound,
|
||||
AlreadyExists,
|
||||
InvalidState(String),
|
||||
StorageError(String),
|
||||
TranscriptionError(String),
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RecordingError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
|
||||
Self::NotFound => write!(f, "Recording not found"),
|
||||
Self::AlreadyExists => write!(f, "Recording already exists"),
|
||||
Self::InvalidState(s) => write!(f, "Invalid state: {s}"),
|
||||
Self::StorageError(e) => write!(f, "Storage error: {e}"),
|
||||
Self::TranscriptionError(e) => write!(f, "Transcription error: {e}"),
|
||||
Self::Unauthorized => write!(f, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecordingError {}
|
||||
|
||||
use super::webinar::{
|
||||
RecordingQuality, RecordingStatus, TranscriptionFormat, TranscriptionSegment,
|
||||
TranscriptionStatus, TranscriptionWord, WebinarRecording, WebinarTranscription,
|
||||
};
|
||||
|
||||
/// Maximum recording duration in seconds (8 hours)
|
||||
const MAX_RECORDING_DURATION_SECONDS: u64 = 28800;
|
||||
|
||||
/// Default transcription language
|
||||
const DEFAULT_TRANSCRIPTION_LANGUAGE: &str = "en-US";
|
||||
|
||||
/// Supported transcription languages
|
||||
const SUPPORTED_LANGUAGES: &[&str] = &[
|
||||
"en-US", "en-GB", "es-ES", "es-MX", "fr-FR", "de-DE", "it-IT", "pt-BR", "pt-PT", "nl-NL",
|
||||
"pl-PL", "ru-RU", "ja-JP", "ko-KR", "zh-CN", "zh-TW", "ar-SA", "hi-IN", "tr-TR", "vi-VN",
|
||||
];
|
||||
|
||||
/// Configuration for recording service
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RecordingConfig {
|
||||
/// Maximum recording duration in seconds
|
||||
pub max_duration_seconds: u64,
|
||||
/// Default recording quality
|
||||
pub default_quality: RecordingQuality,
|
||||
/// Storage backend (local, s3, azure, gcs)
|
||||
pub storage_backend: StorageBackend,
|
||||
/// Storage bucket/container name
|
||||
pub storage_bucket: String,
|
||||
/// Enable automatic transcription
|
||||
pub auto_transcribe: bool,
|
||||
/// Default transcription language
|
||||
pub default_language: String,
|
||||
/// Transcription provider
|
||||
pub transcription_provider: TranscriptionProvider,
|
||||
/// Recording retention days (0 = indefinite)
|
||||
pub retention_days: u32,
|
||||
/// Enable speaker diarization
|
||||
pub speaker_diarization: bool,
|
||||
/// Maximum speakers to identify
|
||||
pub max_speakers: u8,
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +87,6 @@ impl Default for RecordingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Storage backend options
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub enum StorageBackend {
|
||||
#[default]
|
||||
|
|
@ -100,7 +107,6 @@ impl std::fmt::Display for StorageBackend {
|
|||
}
|
||||
}
|
||||
|
||||
/// Transcription provider options
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub enum TranscriptionProvider {
|
||||
#[default]
|
||||
|
|
@ -125,7 +131,6 @@ impl std::fmt::Display for TranscriptionProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recording session state
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RecordingSession {
|
||||
pub id: Uuid,
|
||||
|
|
@ -143,7 +148,6 @@ pub struct RecordingSession {
|
|||
pub bytes_written: u64,
|
||||
}
|
||||
|
||||
/// Transcription job state
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TranscriptionJob {
|
||||
pub id: Uuid,
|
||||
|
|
@ -161,7 +165,6 @@ pub struct TranscriptionJob {
|
|||
pub retry_count: u8,
|
||||
}
|
||||
|
||||
/// Recording event types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RecordingEvent {
|
||||
Started {
|
||||
|
|
@ -216,7 +219,6 @@ pub enum RecordingEvent {
|
|||
},
|
||||
}
|
||||
|
||||
/// Request to start recording
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StartRecordingRequest {
|
||||
pub webinar_id: Uuid,
|
||||
|
|
@ -226,14 +228,12 @@ pub struct StartRecordingRequest {
|
|||
pub speaker_diarization: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request to stop recording
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StopRecordingRequest {
|
||||
pub recording_id: Uuid,
|
||||
pub start_transcription: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request to get transcription in specific format
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportTranscriptionRequest {
|
||||
pub format: TranscriptionFormat,
|
||||
|
|
@ -242,7 +242,6 @@ pub struct ExportTranscriptionRequest {
|
|||
pub max_line_length: Option<usize>,
|
||||
}
|
||||
|
||||
/// Response for transcription export
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportTranscriptionResponse {
|
||||
pub format: TranscriptionFormat,
|
||||
|
|
@ -251,7 +250,6 @@ pub struct ExportTranscriptionResponse {
|
|||
pub filename: String,
|
||||
}
|
||||
|
||||
/// Recording service for managing webinar recordings and transcriptions
|
||||
pub struct RecordingService {
|
||||
pool: DbPool,
|
||||
config: RecordingConfig,
|
||||
|
|
@ -272,12 +270,10 @@ impl RecordingService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Subscribe to recording events
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<RecordingEvent> {
|
||||
self.event_sender.subscribe()
|
||||
}
|
||||
|
||||
/// Start recording a webinar
|
||||
pub async fn start_recording(
|
||||
&self,
|
||||
request: StartRecordingRequest,
|
||||
|
|
@ -352,7 +348,6 @@ impl RecordingService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Pause recording
|
||||
pub async fn pause_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||
let mut sessions = self.active_sessions.write().await;
|
||||
let session = sessions
|
||||
|
|
@ -372,7 +367,6 @@ impl RecordingService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume recording
|
||||
pub async fn resume_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||
let mut sessions = self.active_sessions.write().await;
|
||||
let session = sessions
|
||||
|
|
@ -390,7 +384,6 @@ impl RecordingService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop recording and optionally start transcription
|
||||
pub async fn stop_recording(
|
||||
&self,
|
||||
request: StopRecordingRequest,
|
||||
|
|
@ -452,7 +445,6 @@ impl RecordingService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Process recording (convert, compress, generate thumbnails)
|
||||
async fn process_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||
let _ = self
|
||||
.event_sender
|
||||
|
|
@ -484,7 +476,6 @@ impl RecordingService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Start transcription for a recording
|
||||
pub async fn start_transcription(
|
||||
&self,
|
||||
recording_id: Uuid,
|
||||
|
|
@ -560,7 +551,6 @@ impl RecordingService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Run the transcription process
|
||||
async fn run_transcription(&self, transcription_id: Uuid, recording_id: Uuid) {
|
||||
// Update status to in progress
|
||||
{
|
||||
|
|
@ -673,7 +663,6 @@ impl RecordingService {
|
|||
});
|
||||
}
|
||||
|
||||
/// Get recording by ID
|
||||
pub async fn get_recording(&self, recording_id: Uuid) -> Result<WebinarRecording, RecordingError> {
|
||||
// Check active sessions first
|
||||
let sessions = self.active_sessions.read().await;
|
||||
|
|
@ -709,7 +698,6 @@ impl RecordingService {
|
|||
self.get_recording_from_db(recording_id).await
|
||||
}
|
||||
|
||||
/// Get transcription by ID
|
||||
pub async fn get_transcription(
|
||||
&self,
|
||||
transcription_id: Uuid,
|
||||
|
|
@ -742,7 +730,6 @@ impl RecordingService {
|
|||
self.get_transcription_from_db(transcription_id).await
|
||||
}
|
||||
|
||||
/// Export transcription in specified format
|
||||
pub async fn export_transcription(
|
||||
&self,
|
||||
transcription_id: Uuid,
|
||||
|
|
@ -782,7 +769,6 @@ impl RecordingService {
|
|||
})
|
||||
}
|
||||
|
||||
/// Format transcription as plain text
|
||||
fn format_as_plain_text(
|
||||
&self,
|
||||
transcription: &WebinarTranscription,
|
||||
|
|
@ -810,7 +796,6 @@ impl RecordingService {
|
|||
output
|
||||
}
|
||||
|
||||
/// Format transcription as VTT (WebVTT)
|
||||
fn format_as_vtt(
|
||||
&self,
|
||||
transcription: &WebinarTranscription,
|
||||
|
|
@ -838,7 +823,6 @@ impl RecordingService {
|
|||
output
|
||||
}
|
||||
|
||||
/// Format transcription as SRT
|
||||
fn format_as_srt(
|
||||
&self,
|
||||
transcription: &WebinarTranscription,
|
||||
|
|
@ -866,7 +850,6 @@ impl RecordingService {
|
|||
output
|
||||
}
|
||||
|
||||
/// List recordings for a webinar
|
||||
pub async fn list_recordings(
|
||||
&self,
|
||||
webinar_id: Uuid,
|
||||
|
|
@ -874,7 +857,6 @@ impl RecordingService {
|
|||
self.list_recordings_from_db(webinar_id).await
|
||||
}
|
||||
|
||||
/// Delete a recording
|
||||
pub async fn delete_recording(&self, recording_id: Uuid) -> Result<(), RecordingError> {
|
||||
// Check if recording is active
|
||||
let sessions = self.active_sessions.read().await;
|
||||
|
|
|
|||
|
|
@ -1596,7 +1596,7 @@ async fn create_webinar_handler(
|
|||
host_id: Uuid,
|
||||
Json(request): Json<CreateWebinarRequest>,
|
||||
) -> Result<Json<Webinar>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let webinar = service.create_webinar(organization_id, host_id, request).await?;
|
||||
Ok(Json(webinar))
|
||||
}
|
||||
|
|
@ -1605,7 +1605,7 @@ async fn get_webinar_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(webinar_id): Path<Uuid>,
|
||||
) -> Result<Json<Webinar>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let webinar = service.get_webinar(webinar_id).await?;
|
||||
Ok(Json(webinar))
|
||||
}
|
||||
|
|
@ -1615,7 +1615,7 @@ async fn start_webinar_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
host_id: Uuid,
|
||||
) -> Result<Json<Webinar>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let webinar = service.start_webinar(webinar_id, host_id).await?;
|
||||
Ok(Json(webinar))
|
||||
}
|
||||
|
|
@ -1625,7 +1625,7 @@ async fn end_webinar_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
host_id: Uuid,
|
||||
) -> Result<Json<Webinar>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let webinar = service.end_webinar(webinar_id, host_id).await?;
|
||||
Ok(Json(webinar))
|
||||
}
|
||||
|
|
@ -1635,7 +1635,7 @@ async fn register_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
Json(request): Json<RegisterRequest>,
|
||||
) -> Result<Json<WebinarRegistration>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let registration = service.register_attendee(webinar_id, request).await?;
|
||||
Ok(Json(registration))
|
||||
}
|
||||
|
|
@ -1645,7 +1645,7 @@ async fn join_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
participant_id: Uuid,
|
||||
) -> Result<Json<WebinarParticipant>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let participant = service.join_webinar(webinar_id, participant_id).await?;
|
||||
Ok(Json(participant))
|
||||
}
|
||||
|
|
@ -1655,7 +1655,7 @@ async fn raise_hand_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
participant_id: Uuid,
|
||||
) -> Result<StatusCode, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
service.raise_hand(webinar_id, participant_id).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
|
@ -1665,7 +1665,7 @@ async fn lower_hand_handler(
|
|||
Path(webinar_id): Path<Uuid>,
|
||||
participant_id: Uuid,
|
||||
) -> Result<StatusCode, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
service.lower_hand(webinar_id, participant_id).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
|
@ -1674,7 +1674,7 @@ async fn get_raised_hands_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(webinar_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<WebinarParticipant>>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let hands = service.get_raised_hands(webinar_id).await?;
|
||||
Ok(Json(hands))
|
||||
}
|
||||
|
|
@ -1683,7 +1683,7 @@ async fn get_questions_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(webinar_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<QAQuestion>>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let questions = service.get_questions(webinar_id, false).await?;
|
||||
Ok(Json(questions))
|
||||
}
|
||||
|
|
@ -1694,7 +1694,7 @@ async fn submit_question_handler(
|
|||
asker_id: Option<Uuid>,
|
||||
Json(request): Json<SubmitQuestionRequest>,
|
||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let question = service.submit_question(webinar_id, asker_id, "Anonymous".to_string(), request).await?;
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
|
@ -1705,7 +1705,7 @@ async fn answer_question_handler(
|
|||
answerer_id: Uuid,
|
||||
Json(request): Json<AnswerQuestionRequest>,
|
||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let question = service.answer_question(question_id, answerer_id, request).await?;
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
|
@ -1715,7 +1715,7 @@ async fn upvote_question_handler(
|
|||
Path((webinar_id, question_id)): Path<(Uuid, Uuid)>,
|
||||
voter_id: Uuid,
|
||||
) -> Result<Json<QAQuestion>, WebinarError> {
|
||||
let service = WebinarService::new(state.db_pool.clone());
|
||||
let service = WebinarService::new(state.conn.clone());
|
||||
let question = service.upvote_question(question_id, voter_id).await?;
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::sync::Arc;
|
|||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
pub mod import;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,19 @@ use std::sync::{Arc, RwLock};
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
const CHALLENGE_TIMEOUT_SECONDS: i64 = 300;
|
||||
const PASSKEY_NAME_MAX_LENGTH: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct FallbackAttemptTracker {
|
||||
attempts: u32,
|
||||
first_attempt_at: DateTime<Utc>,
|
||||
locked_until: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub struct PasskeyCredential {
|
||||
pub id: String,
|
||||
pub user_id: Uuid,
|
||||
|
|
@ -195,14 +203,12 @@ pub struct RegistrationResult {
|
|||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Request for password fallback authentication when passkey is unavailable
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PasswordFallbackRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Response for password fallback authentication
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PasswordFallbackResponse {
|
||||
pub success: bool,
|
||||
|
|
@ -212,18 +218,12 @@ pub struct PasswordFallbackResponse {
|
|||
pub passkey_available: bool,
|
||||
}
|
||||
|
||||
/// Configuration for fallback authentication behavior
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FallbackConfig {
|
||||
/// Whether password fallback is enabled
|
||||
pub enabled: bool,
|
||||
/// Require additional verification after password fallback
|
||||
pub require_additional_verification: bool,
|
||||
/// Maximum password fallback attempts before lockout
|
||||
pub max_fallback_attempts: u32,
|
||||
/// Lockout duration in seconds after max attempts
|
||||
pub lockout_duration_seconds: u64,
|
||||
/// Prompt user to set up passkey after password login
|
||||
pub prompt_passkey_setup: bool,
|
||||
}
|
||||
|
||||
|
|
@ -291,7 +291,6 @@ impl PasskeyService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new PasskeyService with custom fallback configuration
|
||||
pub fn with_fallback_config(
|
||||
pool: DbPool,
|
||||
rp_id: String,
|
||||
|
|
@ -311,17 +310,11 @@ impl PasskeyService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if user has any registered passkeys
|
||||
pub async fn user_has_passkeys(&self, username: &str) -> Result<bool, PasskeyError> {
|
||||
let passkeys = self.get_passkeys_by_username(username).await?;
|
||||
Ok(!passkeys.is_empty())
|
||||
}
|
||||
|
||||
/// Authenticate using password fallback when passkey is unavailable
|
||||
/// This is used when:
|
||||
/// 1. User's device doesn't support passkeys
|
||||
/// 2. User hasn't set up passkeys yet
|
||||
/// 3. Passkey authentication failed and fallback is enabled
|
||||
pub async fn authenticate_with_password_fallback(
|
||||
&self,
|
||||
request: &PasswordFallbackRequest,
|
||||
|
|
@ -384,7 +377,6 @@ impl PasskeyService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if user is locked out due to too many failed attempts
|
||||
async fn is_user_locked_out(&self, username: &str) -> bool {
|
||||
let attempts = self.fallback_attempts.read().await;
|
||||
if let Some(tracker) = attempts.get(username) {
|
||||
|
|
@ -395,7 +387,6 @@ impl PasskeyService {
|
|||
false
|
||||
}
|
||||
|
||||
/// Track a failed fallback attempt
|
||||
async fn track_fallback_attempt(&self, username: &str) {
|
||||
let mut attempts = self.fallback_attempts.write().await;
|
||||
let now = Utc::now();
|
||||
|
|
@ -416,32 +407,25 @@ impl PasskeyService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Clear fallback attempts after successful login
|
||||
async fn clear_fallback_attempts(&self, username: &str) {
|
||||
let mut attempts = self.fallback_attempts.write().await;
|
||||
attempts.remove(username);
|
||||
}
|
||||
|
||||
/// Verify password against database
|
||||
async fn verify_password(&self, username: &str, password: &str) -> Result<Uuid, PasskeyError> {
|
||||
let mut conn = self.pool.get().map_err(|_| PasskeyError::DatabaseError)?;
|
||||
|
||||
// Query user by username
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, password_hash
|
||||
FROM users
|
||||
WHERE username = $1 OR email = $1
|
||||
"#,
|
||||
username
|
||||
let result: Option<(Uuid, Option<String>)> = diesel::sql_query(
|
||||
"SELECT id, password_hash FROM users WHERE username = $1 OR email = $1"
|
||||
)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await;
|
||||
.bind::<Text, _>(username)
|
||||
.get_result::<(Uuid, Option<String>)>(&mut conn)
|
||||
.optional()
|
||||
.map_err(|_| PasskeyError::DatabaseError)?;
|
||||
|
||||
match result {
|
||||
Ok(Some(user)) => {
|
||||
// Verify password hash using argon2
|
||||
if let Some(hash) = user.password_hash {
|
||||
Some((user_id, password_hash)) => {
|
||||
if let Some(hash) = password_hash {
|
||||
let parsed_hash = argon2::PasswordHash::new(&hash)
|
||||
.map_err(|_| PasskeyError::InvalidCredentialId)?;
|
||||
|
||||
|
|
@ -449,17 +433,15 @@ impl PasskeyService {
|
|||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(user.id);
|
||||
return Ok(user_id);
|
||||
}
|
||||
}
|
||||
Err(PasskeyError::InvalidCredentialId)
|
||||
}
|
||||
Ok(None) => Err(PasskeyError::InvalidCredentialId),
|
||||
Err(_) => Err(PasskeyError::DatabaseError),
|
||||
None => Err(PasskeyError::InvalidCredentialId),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a session token for authenticated user
|
||||
fn generate_session_token(&self, user_id: &Uuid) -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
|
@ -471,7 +453,6 @@ impl PasskeyService {
|
|||
format!("{}:{}", user_id, token)
|
||||
}
|
||||
|
||||
/// Check if password fallback should be offered based on passkey availability
|
||||
pub async fn should_offer_password_fallback(&self, username: &str) -> Result<bool, PasskeyError> {
|
||||
if !self.fallback_config.enabled {
|
||||
return Ok(false);
|
||||
|
|
@ -482,12 +463,10 @@ impl PasskeyService {
|
|||
Ok(!has_passkeys || self.fallback_config.enabled)
|
||||
}
|
||||
|
||||
/// Get fallback configuration
|
||||
pub fn get_fallback_config(&self) -> &FallbackConfig {
|
||||
&self.fallback_config
|
||||
}
|
||||
|
||||
/// Update fallback configuration
|
||||
pub fn set_fallback_config(&mut self, config: FallbackConfig) {
|
||||
self.fallback_config = config;
|
||||
}
|
||||
|
|
@ -1303,7 +1282,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
|||
.route("/fallback/config", get(get_fallback_config_handler))
|
||||
}
|
||||
|
||||
/// Handler for password fallback authentication
|
||||
async fn password_fallback_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<PasswordFallbackRequest>,
|
||||
|
|
@ -1315,7 +1293,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handler to check if password fallback is available for a user
|
||||
async fn check_fallback_available_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(username): Path<String>,
|
||||
|
|
@ -1346,7 +1323,6 @@ pub fn passkey_routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handler to get fallback configuration (public settings only)
|
||||
async fn get_fallback_config_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
|
|
@ -1438,7 +1414,7 @@ struct RegistrationVerifyRequest {
|
|||
}
|
||||
|
||||
fn get_passkey_service(state: &AppState) -> Result<PasskeyService, PasskeyError> {
|
||||
let pool = state.db_pool.clone();
|
||||
let pool = state.conn.clone();
|
||||
let rp_id = std::env::var("PASSKEY_RP_ID").unwrap_or_else(|_| "localhost".to_string());
|
||||
let rp_name = std::env::var("PASSKEY_RP_NAME").unwrap_or_else(|_| "General Bots".to_string());
|
||||
let rp_origin = std::env::var("PASSKEY_RP_ORIGIN").unwrap_or_else(|_| "http://localhost:8081".to_string());
|
||||
|
|
|
|||
|
|
@ -1111,39 +1111,35 @@ fn render_post_card_html(post: &PostWithAuthor) -> String {
|
|||
.post
|
||||
.reaction_counts
|
||||
.iter()
|
||||
.map(|(emoji, count)| format!(r#"<span class="reaction">{emoji} {count}</span>"#))
|
||||
.map(|(emoji, count)| format!("<span class=\"reaction\">{emoji} {count}</span>"))
|
||||
.collect();
|
||||
|
||||
let avatar_url = post.author.avatar_url.as_deref().unwrap_or("/assets/default-avatar.svg");
|
||||
let post_time = post.post.created_at.format("%b %d, %Y");
|
||||
|
||||
format!(
|
||||
concat!(
|
||||
r#"<article class="post-card" data-post-id="{}">"#,
|
||||
r#"<header class="post-header">"#,
|
||||
r#"<img class="avatar" src="{}" alt="{}" />"#,
|
||||
r#"<div class="post-meta"><span class="author-name">{}</span><span class="post-time">{}</span></div>"#,
|
||||
r#"</header>"#,
|
||||
r#"<div class="post-content">{}</div>"#,
|
||||
r#"<footer class="post-footer">"#,
|
||||
r#"<div class="reactions">{}</div>"#,
|
||||
r#"<div class="post-actions">"#,
|
||||
r#"<button class="btn-react" hx-post="/api/social/posts/{}/react" hx-swap="outerHTML">👍</button>"#,
|
||||
r#"<button class="btn-comment" hx-get="/api/social/posts/{}/comments" hx-target="#comments-{}">💬 {}</button>"#,
|
||||
r#"</div>"#,
|
||||
r#"</footer>"#,
|
||||
r##"<div id="comments-{}" class="comments-section"></div>"##,
|
||||
r#"</article>"#
|
||||
),
|
||||
post.post.id,
|
||||
post.author.avatar_url.as_deref().unwrap_or("/assets/default-avatar.svg"),
|
||||
post.author.name,
|
||||
post.author.name,
|
||||
post.post.created_at.format("%b %d, %Y"),
|
||||
post.post.content,
|
||||
reactions_html,
|
||||
post.post.id,
|
||||
post.post.id,
|
||||
post.post.id,
|
||||
post.post.comment_count,
|
||||
post.post.id
|
||||
"<article class=\"post-card\" data-post-id=\"{id}\">\
|
||||
<header class=\"post-header\">\
|
||||
<img class=\"avatar\" src=\"{avatar}\" alt=\"{name}\" />\
|
||||
<div class=\"post-meta\"><span class=\"author-name\">{name}</span><span class=\"post-time\">{time}</span></div>\
|
||||
</header>\
|
||||
<div class=\"post-content\">{content}</div>\
|
||||
<footer class=\"post-footer\">\
|
||||
<div class=\"reactions\">{reactions}</div>\
|
||||
<div class=\"post-actions\">\
|
||||
<button class=\"btn-react\" hx-post=\"/api/social/posts/{id}/react\" hx-swap=\"outerHTML\">Like</button>\
|
||||
<button class=\"btn-comment\" hx-get=\"/api/social/posts/{id}/comments\" hx-target=\"#comments-{id}\">Comment {comments}</button>\
|
||||
</div>\
|
||||
</footer>\
|
||||
<div id=\"comments-{id}\" class=\"comments-section\"></div>\
|
||||
</article>",
|
||||
id = post.post.id,
|
||||
avatar = avatar_url,
|
||||
name = post.author.name,
|
||||
time = post_time,
|
||||
content = post.post.content,
|
||||
reactions = reactions_html,
|
||||
comments = post.post.comment_count,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ impl VectorDBIndexer {
|
|||
}
|
||||
|
||||
async fn get_active_users(&self) -> Result<Vec<(Uuid, Uuid)>> {
|
||||
let conn = self.db_pool.clone();
|
||||
let conn = self.conn.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
use crate::shared::models::schema::user_sessions::dsl::*;
|
||||
|
|
@ -395,7 +395,7 @@ impl VectorDBIndexer {
|
|||
}
|
||||
|
||||
async fn get_user_email_accounts(&self, user_id: Uuid) -> Result<Vec<String>> {
|
||||
let conn = self.db_pool.clone();
|
||||
let conn = self.conn.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
use diesel::prelude::*;
|
||||
|
|
@ -427,7 +427,7 @@ impl VectorDBIndexer {
|
|||
user_id: Uuid,
|
||||
account_id: &str,
|
||||
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let pool = self.db_pool.clone();
|
||||
let pool = self.conn.clone();
|
||||
let account_id = account_id.to_string();
|
||||
|
||||
let results = tokio::task::spawn_blocking(move || {
|
||||
|
|
@ -504,7 +504,7 @@ impl VectorDBIndexer {
|
|||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let pool = self.db_pool.clone();
|
||||
let pool = self.conn.clone();
|
||||
|
||||
let results = tokio::task::spawn_blocking(move || {
|
||||
use diesel::prelude::*;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use diesel::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::security::error_sanitizer::SafeErrorResponse;
|
||||
use crate::shared::state::AppState;
|
||||
use crate::shared::utils::DbPool;
|
||||
|
||||
use super::models::*;
|
||||
|
|
@ -296,20 +303,11 @@ fn parse_devices(json: &Option<serde_json::Value>) -> DeviceBreakdown {
|
|||
}
|
||||
}
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
|
||||
use crate::security::error_sanitizer::SafeErrorResponse;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
pub async fn get_analytics_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = AnalyticsEngine::new(state.db.clone());
|
||||
let engine = AnalyticsEngine::new(state.conn.clone());
|
||||
|
||||
let _ = engine.get_or_create_analytics(project_id, None).await;
|
||||
|
||||
|
|
@ -329,7 +327,7 @@ pub async fn record_view_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RecordViewRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = AnalyticsEngine::new(state.db.clone());
|
||||
let engine = AnalyticsEngine::new(state.conn.clone());
|
||||
|
||||
match engine.record_view(req).await {
|
||||
Ok(_) => (
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub async fn list_projects(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Query(filters): Query<ProjectFilters>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.list_projects(None, filters).await {
|
||||
Ok(projects) => (
|
||||
StatusCode::OK,
|
||||
|
|
@ -37,7 +37,7 @@ pub async fn create_project(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateProjectRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.create_project(None, None, req).await {
|
||||
Ok(project) => (
|
||||
StatusCode::CREATED,
|
||||
|
|
@ -57,7 +57,7 @@ pub async fn get_project(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.get_project_detail(id).await {
|
||||
Ok(detail) => (StatusCode::OK, Json(serde_json::json!(detail))),
|
||||
Err(diesel::result::Error::NotFound) => (
|
||||
|
|
@ -79,7 +79,7 @@ pub async fn update_project(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<UpdateProjectRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.update_project(id, req).await {
|
||||
Ok(project) => (
|
||||
StatusCode::OK,
|
||||
|
|
@ -103,7 +103,7 @@ pub async fn delete_project(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.delete_project(id).await {
|
||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||
Err(e) => {
|
||||
|
|
@ -120,7 +120,7 @@ pub async fn get_clips(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.get_clips(project_id).await {
|
||||
Ok(clips) => (StatusCode::OK, Json(serde_json::json!({ "clips": clips }))),
|
||||
Err(e) => {
|
||||
|
|
@ -138,7 +138,7 @@ pub async fn add_clip(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<AddClipRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.add_clip(project_id, req).await {
|
||||
Ok(clip) => (StatusCode::CREATED, Json(serde_json::json!({ "clip": clip }))),
|
||||
Err(e) => {
|
||||
|
|
@ -156,7 +156,7 @@ pub async fn update_clip(
|
|||
Path(clip_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateClipRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.update_clip(clip_id, req).await {
|
||||
Ok(clip) => (StatusCode::OK, Json(serde_json::json!({ "clip": clip }))),
|
||||
Err(diesel::result::Error::NotFound) => (
|
||||
|
|
@ -177,7 +177,7 @@ pub async fn delete_clip(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(clip_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.delete_clip(clip_id).await {
|
||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||
Err(e) => {
|
||||
|
|
@ -195,7 +195,7 @@ pub async fn split_clip_handler(
|
|||
Path(clip_id): Path<Uuid>,
|
||||
Json(req): Json<SplitClipRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.split_clip(clip_id, req.at_ms).await {
|
||||
Ok((first, second)) => (
|
||||
StatusCode::OK,
|
||||
|
|
@ -222,7 +222,7 @@ pub async fn get_layers(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.get_layers(project_id).await {
|
||||
Ok(layers) => (StatusCode::OK, Json(serde_json::json!({ "layers": layers }))),
|
||||
Err(e) => {
|
||||
|
|
@ -240,7 +240,7 @@ pub async fn add_layer(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<AddLayerRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.add_layer(project_id, req).await {
|
||||
Ok(layer) => (
|
||||
StatusCode::CREATED,
|
||||
|
|
@ -261,7 +261,7 @@ pub async fn update_layer(
|
|||
Path(layer_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateLayerRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.update_layer(layer_id, req).await {
|
||||
Ok(layer) => (StatusCode::OK, Json(serde_json::json!({ "layer": layer }))),
|
||||
Err(diesel::result::Error::NotFound) => (
|
||||
|
|
@ -282,7 +282,7 @@ pub async fn delete_layer(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(layer_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.delete_layer(layer_id).await {
|
||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||
Err(e) => {
|
||||
|
|
@ -299,7 +299,7 @@ pub async fn get_audio_tracks(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.get_audio_tracks(project_id).await {
|
||||
Ok(tracks) => (
|
||||
StatusCode::OK,
|
||||
|
|
@ -320,7 +320,7 @@ pub async fn add_audio_track(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<AddAudioRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.add_audio_track(project_id, req).await {
|
||||
Ok(track) => (
|
||||
StatusCode::CREATED,
|
||||
|
|
@ -340,7 +340,7 @@ pub async fn delete_audio_track(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(track_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.delete_audio_track(track_id).await {
|
||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||
Err(e) => {
|
||||
|
|
@ -357,7 +357,7 @@ pub async fn get_keyframes(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(layer_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.get_keyframes(layer_id).await {
|
||||
Ok(keyframes) => (
|
||||
StatusCode::OK,
|
||||
|
|
@ -378,7 +378,7 @@ pub async fn add_keyframe(
|
|||
Path(layer_id): Path<Uuid>,
|
||||
Json(req): Json<AddKeyframeRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.add_keyframe(layer_id, req).await {
|
||||
Ok(keyframe) => (
|
||||
StatusCode::CREATED,
|
||||
|
|
@ -398,7 +398,7 @@ pub async fn delete_keyframe(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(keyframe_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine.delete_keyframe(keyframe_id).await {
|
||||
Ok(()) => (StatusCode::NO_CONTENT, Json(serde_json::json!({}))),
|
||||
Err(e) => {
|
||||
|
|
@ -423,7 +423,7 @@ pub async fn upload_media(
|
|||
error!("Failed to create upload directory: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("upload_media") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +457,7 @@ pub async fn upload_media(
|
|||
error!("Failed to write uploaded file: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("upload_media") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -501,7 +501,7 @@ pub async fn get_preview_frame(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Query(params): Query<PreviewFrameRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
let at_ms = params.at_ms.unwrap_or(0);
|
||||
let width = params.width.unwrap_or(640);
|
||||
let height = params.height.unwrap_or(360);
|
||||
|
|
@ -513,7 +513,7 @@ pub async fn get_preview_frame(
|
|||
error!("Failed to create preview directory: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("get_preview_frame") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -529,7 +529,7 @@ pub async fn get_preview_frame(
|
|||
error!("Failed to generate preview: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("get_preview_frame") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -540,7 +540,7 @@ pub async fn transcribe_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<TranscribeRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
match engine
|
||||
.transcribe_audio(project_id, req.clip_id, req.language)
|
||||
.await
|
||||
|
|
@ -561,7 +561,7 @@ pub async fn generate_captions_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<GenerateCaptionsRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
let transcription = match engine.transcribe_audio(project_id, None, None).await {
|
||||
Ok(t) => t,
|
||||
|
|
@ -569,7 +569,7 @@ pub async fn generate_captions_handler(
|
|||
error!("Transcription failed: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("generate_captions") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -614,7 +614,7 @@ pub async fn tts_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<TTSRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
let output_dir =
|
||||
std::env::var("VIDEO_AUDIO_DIR").unwrap_or_else(|_| "./audio/video".to_string());
|
||||
|
||||
|
|
@ -622,7 +622,7 @@ pub async fn tts_handler(
|
|||
error!("Failed to create audio directory: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("tts") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -661,7 +661,7 @@ pub async fn tts_handler(
|
|||
error!("Failed to add audio track: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("tts") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -680,7 +680,7 @@ pub async fn detect_scenes_handler(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
let output_dir =
|
||||
std::env::var("VIDEO_THUMBNAILS_DIR").unwrap_or_else(|_| "./thumbnails/video".to_string());
|
||||
|
||||
|
|
@ -688,7 +688,7 @@ pub async fn detect_scenes_handler(
|
|||
error!("Failed to create thumbnails directory: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("detect_scenes") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -709,7 +709,7 @@ pub async fn auto_reframe_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<AutoReframeRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
let clips = match engine.get_clips(project_id).await {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
|
|
@ -736,7 +736,7 @@ pub async fn auto_reframe_handler(
|
|||
error!("Failed to create reframe directory: {e}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": sanitize_error("auto_reframe") })),
|
||||
Json(serde_json::json!({ "error": SafeErrorResponse::internal_error() })),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -769,7 +769,7 @@ pub async fn remove_background_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<BackgroundRemovalRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.remove_background(project_id, req.clip_id, req.replacement)
|
||||
|
|
@ -791,7 +791,7 @@ pub async fn enhance_video_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<VideoEnhanceRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine.enhance_video(project_id, req).await {
|
||||
Ok(response) => (StatusCode::OK, Json(serde_json::json!(response))),
|
||||
|
|
@ -810,7 +810,7 @@ pub async fn beat_sync_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<BeatSyncRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.detect_beats(project_id, req.audio_track_id, req.sensitivity)
|
||||
|
|
@ -832,7 +832,7 @@ pub async fn generate_waveform_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<WaveformRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.generate_waveform(project_id, req.audio_track_id, req.samples_per_second)
|
||||
|
|
@ -896,7 +896,7 @@ pub async fn apply_template_handler(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<ApplyTemplateRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.apply_template(project_id, &req.template_id, req.customizations)
|
||||
|
|
@ -921,7 +921,7 @@ pub async fn add_transition_handler(
|
|||
Path((from_id, to_id)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<TransitionRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.add_transition(from_id, to_id, &req.transition_type, req.duration_ms)
|
||||
|
|
@ -946,7 +946,7 @@ pub async fn chat_edit(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<ChatEditRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine
|
||||
.process_chat_command(project_id, &req.message, req.playhead_ms, req.selection)
|
||||
|
|
@ -973,7 +973,7 @@ pub async fn start_export(
|
|||
Path(project_id): Path<Uuid>,
|
||||
Json(req): Json<ExportRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine.start_export(project_id, req, state.cache.as_ref()).await {
|
||||
Ok(export) => (
|
||||
|
|
@ -994,7 +994,7 @@ pub async fn get_export_status(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(export_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let engine = VideoEngine::new(state.db.clone());
|
||||
let engine = VideoEngine::new(state.conn.clone());
|
||||
|
||||
match engine.get_export_status(export_id).await {
|
||||
Ok(export) => (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub use analytics::{get_analytics_handler, record_view_handler, AnalyticsEngine}
|
|||
pub use engine::VideoEngine;
|
||||
pub use handlers::*;
|
||||
pub use models::*;
|
||||
pub use render::{start_render_worker, start_render_worker_with_broadcaster, VideoRenderWorker};
|
||||
pub use render::{start_render_worker, VideoRenderWorker};
|
||||
pub use schema::*;
|
||||
pub use websocket::{broadcast_export_progress, export_progress_websocket, ExportProgressBroadcaster};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ use crate::shared::utils::DbPool;
|
|||
|
||||
use super::models::*;
|
||||
use super::schema::*;
|
||||
use super::websocket::{broadcast_export_progress, ExportProgressBroadcaster};
|
||||
use super::websocket::broadcast_export_progress;
|
||||
|
||||
pub struct VideoRenderWorker {
|
||||
db: DbPool,
|
||||
cache: Arc<redis::Client>,
|
||||
output_dir: String,
|
||||
broadcaster: Option<Arc<ExportProgressBroadcaster>>,
|
||||
}
|
||||
|
||||
impl VideoRenderWorker {
|
||||
|
|
@ -24,21 +23,6 @@ impl VideoRenderWorker {
|
|||
db,
|
||||
cache,
|
||||
output_dir,
|
||||
broadcaster: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_broadcaster(
|
||||
db: DbPool,
|
||||
cache: Arc<redis::Client>,
|
||||
output_dir: String,
|
||||
broadcaster: Arc<ExportProgressBroadcaster>,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
cache,
|
||||
output_dir,
|
||||
broadcaster: Some(broadcaster),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,18 +147,15 @@ impl VideoRenderWorker {
|
|||
.execute(&mut db_conn)?;
|
||||
}
|
||||
|
||||
if let Some(broadcaster) = &self.broadcaster {
|
||||
broadcast_export_progress(
|
||||
broadcaster,
|
||||
export_id,
|
||||
project_id,
|
||||
status,
|
||||
progress,
|
||||
Some(format!("Export {progress}%")),
|
||||
output_url,
|
||||
gbdrive_path,
|
||||
);
|
||||
}
|
||||
broadcast_export_progress(
|
||||
export_id,
|
||||
project_id,
|
||||
status,
|
||||
progress,
|
||||
Some(format!("Export {progress}%")),
|
||||
output_url,
|
||||
gbdrive_path,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -451,15 +432,3 @@ pub fn start_render_worker(db: DbPool, cache: Arc<redis::Client>, output_dir: St
|
|||
worker.run_worker_loop().await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_render_worker_with_broadcaster(
|
||||
db: DbPool,
|
||||
cache: Arc<redis::Client>,
|
||||
output_dir: String,
|
||||
broadcaster: Arc<ExportProgressBroadcaster>,
|
||||
) {
|
||||
let worker = VideoRenderWorker::with_broadcaster(db, cache, output_dir, broadcaster);
|
||||
tokio::spawn(async move {
|
||||
worker.run_worker_loop().await;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ use crate::shared::state::AppState;
|
|||
|
||||
use super::models::ExportProgressEvent;
|
||||
|
||||
static GLOBAL_BROADCASTER: std::sync::OnceLock<Arc<ExportProgressBroadcaster>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
pub struct ExportProgressBroadcaster {
|
||||
tx: broadcast::Sender<ExportProgressEvent>,
|
||||
}
|
||||
|
|
@ -25,6 +28,12 @@ impl ExportProgressBroadcaster {
|
|||
Self { tx }
|
||||
}
|
||||
|
||||
pub fn global() -> Arc<Self> {
|
||||
GLOBAL_BROADCASTER
|
||||
.get_or_init(|| Arc::new(Self::new()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> broadcast::Sender<ExportProgressEvent> {
|
||||
self.tx.clone()
|
||||
}
|
||||
|
|
@ -48,14 +57,14 @@ impl Default for ExportProgressBroadcaster {
|
|||
|
||||
pub async fn export_progress_websocket(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(export_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
info!("WebSocket connection request for export: {export_id}");
|
||||
ws.on_upgrade(move |socket| handle_export_websocket(socket, state, export_id))
|
||||
ws.on_upgrade(move |socket| handle_export_websocket(socket, export_id))
|
||||
}
|
||||
|
||||
async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export_id: Uuid) {
|
||||
async fn handle_export_websocket(socket: WebSocket, export_id: Uuid) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
info!("WebSocket connected for export: {export_id}");
|
||||
|
|
@ -75,13 +84,8 @@ async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export
|
|||
return;
|
||||
}
|
||||
|
||||
let mut progress_rx = if let Some(broadcaster) = state.video_progress_broadcaster.as_ref() {
|
||||
broadcaster.subscribe()
|
||||
} else {
|
||||
let (tx, rx) = broadcast::channel(1);
|
||||
drop(tx);
|
||||
rx
|
||||
};
|
||||
let broadcaster = ExportProgressBroadcaster::global();
|
||||
let mut progress_rx = broadcaster.subscribe();
|
||||
|
||||
let export_id_for_recv = export_id;
|
||||
|
||||
|
|
@ -177,7 +181,6 @@ async fn handle_export_websocket(socket: WebSocket, state: Arc<AppState>, export
|
|||
}
|
||||
|
||||
pub fn broadcast_export_progress(
|
||||
broadcaster: &ExportProgressBroadcaster,
|
||||
export_id: Uuid,
|
||||
project_id: Uuid,
|
||||
status: &str,
|
||||
|
|
@ -196,5 +199,5 @@ pub fn broadcast_export_progress(
|
|||
gbdrive_path,
|
||||
};
|
||||
|
||||
broadcaster.send(event);
|
||||
ExportProgressBroadcaster::global().send(event);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue