olithic route configuration

- Add route configuration and handlers to calendar module
- Add route configuration and handlers to task module
- Update main.rs to build router from module configurations
- Fix various compiler warnings (dead code, unused variables)

This improves code organization by keeping routes co-located with their
implementation logic.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-27 13:53:00 -03:00
parent 6888b5e449
commit 58f19e6450
8 changed files with 846 additions and 459 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
.tmp*
.tmp/*
*.log
target*
.env
@ -9,4 +11,4 @@ botserver-stack
*logfile*
*-log*
docs/book
*.rdb
*.rdb

View file

@ -499,7 +499,7 @@ fn parse_time_string(time_str: &str) -> Result<DateTime<Utc>, String> {
for format in formats {
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(time_str, format) {
return Ok(DateTime::from_utc(dt, Utc));
return Ok(DateTime::from_naive_utc_and_offset(dt, Utc));
}
}
@ -545,7 +545,7 @@ fn parse_date_string(date_str: &str) -> Result<DateTime<Utc>, String> {
for format in formats {
if let Ok(dt) = chrono::NaiveDate::parse_from_str(date_str, format) {
return Ok(dt.and_hms(0, 0, 0).and_utc());
return Ok(dt.and_hms_opt(0, 0, 0).unwrap().and_utc());
}
}

View file

@ -193,7 +193,7 @@ async fn send_message_to_recipient(
match channel.as_str() {
"whatsapp" => {
let adapter = WhatsAppAdapter::new();
let adapter = WhatsAppAdapter::new(state.conn.clone(), user.bot_id);
let response = crate::shared::models::BotResponse {
bot_id: "default".to_string(),
session_id: user.id.to_string(),
@ -211,7 +211,7 @@ async fn send_message_to_recipient(
adapter.send_message(response).await?;
}
"instagram" => {
let adapter = InstagramAdapter::new();
let adapter = InstagramAdapter::new(state.conn.clone(), user.bot_id);
let response = crate::shared::models::BotResponse {
bot_id: "default".to_string(),
session_id: user.id.to_string(),
@ -229,7 +229,7 @@ async fn send_message_to_recipient(
adapter.send_message(response).await?;
}
"teams" => {
let adapter = TeamsAdapter::new();
let adapter = TeamsAdapter::new(state.conn.clone(), user.bot_id);
let response = crate::shared::models::BotResponse {
bot_id: "default".to_string(),
session_id: user.id.to_string(),
@ -274,7 +274,7 @@ async fn send_file_to_recipient(
async fn send_file_with_caption_to_recipient(
state: Arc<AppState>,
_user: &UserSession,
user: &UserSession,
recipient: &str,
file: Dynamic,
caption: &str,
@ -292,13 +292,13 @@ async fn send_file_with_caption_to_recipient(
match channel.as_str() {
"whatsapp" => {
send_whatsapp_file(state, &recipient_id, file_data, caption).await?;
send_whatsapp_file(state, user, &recipient_id, file_data, caption).await?;
}
"instagram" => {
send_instagram_file(state, &recipient_id, file_data, caption).await?;
send_instagram_file(state, user, &recipient_id, file_data, caption).await?;
}
"teams" => {
send_teams_file(state, &recipient_id, file_data, caption).await?;
send_teams_file(state, user, &recipient_id, file_data, caption).await?;
}
"web" => {
send_web_file(state, &recipient_id, file_data, caption).await?;
@ -406,14 +406,15 @@ async fn broadcast_message(
// Channel-specific implementations
async fn send_whatsapp_file(
_state: Arc<AppState>,
state: Arc<AppState>,
user: &UserSession,
recipient: &str,
file_data: Vec<u8>,
caption: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use reqwest::Client;
let _adapter = WhatsAppAdapter::new();
let _adapter = WhatsAppAdapter::new(state.conn.clone(), user.bot_id);
// First, upload the file to WhatsApp
let upload_url = format!(
@ -467,14 +468,15 @@ async fn send_whatsapp_file(
}
async fn send_instagram_file(
_state: Arc<AppState>,
_recipient: &str,
state: Arc<AppState>,
user: &UserSession,
recipient_id: &str,
_file_data: Vec<u8>,
_caption: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Instagram file sending implementation
// Similar to WhatsApp but using Instagram API
let _adapter = InstagramAdapter::new();
let _adapter = InstagramAdapter::new(state.conn.clone(), user.bot_id);
// Upload and send via Instagram Messaging API
@ -483,11 +485,12 @@ async fn send_instagram_file(
async fn send_teams_file(
state: Arc<AppState>,
user: &UserSession,
recipient_id: &str,
file_data: Vec<u8>,
caption: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _adapter = TeamsAdapter::new();
let _adapter = TeamsAdapter::new(state.conn.clone(), user.bot_id);
// Get conversation ID
let conversation_id = get_teams_conversation_id(&state, recipient_id).await?;

View file

@ -420,3 +420,4 @@ pub fn create_media_template(media_type: &str, attachment_id: &str) -> serde_jso
}
})
}

View file

@ -1,10 +1,12 @@
use async_trait::async_trait;
use log::{error, info};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Unused import
use uuid::Uuid;
use crate::core::bot::channels::ChannelAdapter;
use crate::core::config::ConfigManager;
use crate::shared::models::BotResponse;
use crate::shared::utils::DbPool;
#[derive(Debug)]
pub struct TeamsAdapter {
@ -16,21 +18,43 @@ pub struct TeamsAdapter {
}
impl TeamsAdapter {
pub fn new() -> Self {
// Load from environment variables (would be from config.csv in production)
let app_id = std::env::var("TEAMS_APP_ID").unwrap_or_default();
let app_password = std::env::var("TEAMS_APP_PASSWORD").unwrap_or_default();
let tenant_id = std::env::var("TEAMS_TENANT_ID").unwrap_or_default();
let service_url = std::env::var("TEAMS_SERVICE_URL")
.unwrap_or_else(|_| "https://smba.trafficmanager.net".to_string());
let bot_id = std::env::var("TEAMS_BOT_ID").unwrap_or_else(|_| app_id.clone());
pub fn new(pool: DbPool, bot_id: Uuid) -> Self {
let config_manager = ConfigManager::new(pool);
// Load from bot_configuration table with fallback to environment variables
let app_id = config_manager
.get_config(&bot_id, "teams-app-id", None)
.unwrap_or_else(|_| std::env::var("TEAMS_APP_ID").unwrap_or_default());
let app_password = config_manager
.get_config(&bot_id, "teams-app-password", None)
.unwrap_or_else(|_| std::env::var("TEAMS_APP_PASSWORD").unwrap_or_default());
let tenant_id = config_manager
.get_config(&bot_id, "teams-tenant-id", None)
.unwrap_or_else(|_| std::env::var("TEAMS_TENANT_ID").unwrap_or_default());
let service_url = config_manager
.get_config(
&bot_id,
"teams-service-url",
Some("https://smba.trafficmanager.net"),
)
.unwrap_or_else(|_| {
std::env::var("TEAMS_SERVICE_URL")
.unwrap_or_else(|_| "https://smba.trafficmanager.net".to_string())
});
let teams_bot_id = config_manager
.get_config(&bot_id, "teams-bot-id", None)
.unwrap_or_else(|_| std::env::var("TEAMS_BOT_ID").unwrap_or_else(|_| app_id.clone()));
Self {
app_id,
app_password,
tenant_id,
service_url,
bot_id,
bot_id: teams_bot_id,
}
}
@ -40,20 +64,17 @@ impl TeamsAdapter {
let token_url = format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/token",
if self.tenant_id.is_empty() {
"common"
"botframework.com"
} else {
&self.tenant_id
}
);
let params = [
("grant_type", "client_credentials"),
("client_id", &self.app_id),
("client_secret", &self.app_password),
("grant_type", &"client_credentials".to_string()),
(
"scope",
&"https://api.botframework.com/.default".to_string(),
),
("scope", "https://api.botframework.com/.default"),
];
let response = client.post(&token_url).form(&params).send().await?;
@ -66,47 +87,23 @@ impl TeamsAdapter {
.to_string())
} else {
let error_text = response.text().await?;
Err(format!("Failed to get Teams access token: {}", error_text).into())
Err(format!("Failed to get access token: {}", error_text).into())
}
}
async fn send_teams_message(
&self,
conversation_id: &str,
activity_id: Option<&str>,
message: &str,
activity: TeamsActivity,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let token = self.get_access_token().await?;
let client = reqwest::Client::new();
let url = if let Some(reply_to_id) = activity_id {
format!(
"{}/v3/conversations/{}/activities/{}/reply",
self.service_url, conversation_id, reply_to_id
)
} else {
format!(
"{}/v3/conversations/{}/activities",
self.service_url, conversation_id
)
};
let token = self.get_access_token().await?;
let activity = TeamsActivity {
activity_type: "message".to_string(),
text: message.to_string(),
from: TeamsChannelAccount {
id: self.bot_id.clone(),
name: Some("Bot".to_string()),
},
conversation: TeamsConversationAccount {
id: conversation_id.to_string(),
conversation_type: None,
tenant_id: Some(self.tenant_id.clone()),
},
recipient: None,
attachments: None,
entities: None,
};
let url = format!(
"{}/v3/conversations/{}/activities",
self.service_url, conversation_id
);
let response = client
.post(&url)
@ -128,43 +125,88 @@ impl TeamsAdapter {
pub async fn send_card(
&self,
conversation_id: &str,
card: TeamsAdaptiveCard,
card: serde_json::Value,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let token = self.get_access_token().await?;
let client = reqwest::Client::new();
let activity = TeamsActivity {
activity_type: "message".to_string(),
text: None,
attachments: Some(vec![TeamsAttachment {
content_type: "application/vnd.microsoft.card.adaptive".to_string(),
content: card,
}]),
..Default::default()
};
let url = format!(
"{}/v3/conversations/{}/activities",
self.service_url, conversation_id
);
self.send_teams_message(conversation_id, activity).await
}
let attachment = TeamsAttachment {
content_type: "application/vnd.microsoft.card.adaptive".to_string(),
content: serde_json::to_value(card)?,
pub async fn send_hero_card(
&self,
conversation_id: &str,
title: &str,
subtitle: Option<&str>,
text: Option<&str>,
images: Vec<String>,
buttons: Vec<TeamsCardAction>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let hero_card = TeamsHeroCard {
title: Some(title.to_string()),
subtitle: subtitle.map(|s| s.to_string()),
text: text.map(|s| s.to_string()),
images: images
.into_iter()
.map(|url| TeamsCardImage { url, alt: None })
.collect(),
buttons: if buttons.is_empty() {
None
} else {
Some(buttons)
},
};
let activity = TeamsActivity {
activity_type: "message".to_string(),
text: String::new(),
from: TeamsChannelAccount {
id: self.bot_id.clone(),
name: Some("Bot".to_string()),
},
conversation: TeamsConversationAccount {
id: conversation_id.to_string(),
conversation_type: None,
tenant_id: Some(self.tenant_id.clone()),
},
recipient: None,
attachments: Some(vec![attachment]),
entities: None,
text: None,
attachments: Some(vec![TeamsAttachment {
content_type: "application/vnd.microsoft.card.hero".to_string(),
content: serde_json::to_value(hero_card)?,
}]),
..Default::default()
};
self.send_teams_message(conversation_id, activity).await
}
pub async fn create_conversation(
&self,
to: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let token = self.get_access_token().await?;
let url = format!("{}/v3/conversations", self.service_url);
let payload = serde_json::json!({
"bot": {
"id": self.bot_id,
"name": "Bot"
},
"members": [{
"id": to
}],
"channelData": {
"tenant": {
"id": self.tenant_id
}
}
});
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.json(&activity)
.json(&payload)
.send()
.await?;
@ -173,7 +215,7 @@ impl TeamsAdapter {
Ok(result["id"].as_str().unwrap_or("").to_string())
} else {
let error_text = response.text().await?;
Err(format!("Teams API error: {}", error_text).into())
Err(format!("Failed to create conversation: {}", error_text).into())
}
}
@ -181,11 +223,12 @@ impl TeamsAdapter {
&self,
conversation_id: &str,
activity_id: &str,
new_message: &str,
new_text: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let token = self.get_access_token().await?;
let client = reqwest::Client::new();
let token = self.get_access_token().await?;
let url = format!(
"{}/v3/conversations/{}/activities/{}",
self.service_url, conversation_id, activity_id
@ -193,19 +236,8 @@ impl TeamsAdapter {
let activity = TeamsActivity {
activity_type: "message".to_string(),
text: new_message.to_string(),
from: TeamsChannelAccount {
id: self.bot_id.clone(),
name: Some("Bot".to_string()),
},
conversation: TeamsConversationAccount {
id: conversation_id.to_string(),
conversation_type: None,
tenant_id: Some(self.tenant_id.clone()),
},
recipient: None,
attachments: None,
entities: None,
text: Some(new_text.to_string()),
..Default::default()
};
let response = client
@ -218,11 +250,80 @@ impl TeamsAdapter {
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Teams API error: {}", error_text).into());
return Err(format!("Failed to update message: {}", error_text).into());
}
Ok(())
}
pub async fn delete_message(
&self,
conversation_id: &str,
activity_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let token = self.get_access_token().await?;
let url = format!(
"{}/v3/conversations/{}/activities/{}",
self.service_url, conversation_id, activity_id
);
let response = client
.delete(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Failed to delete message: {}", error_text).into());
}
Ok(())
}
pub async fn send_typing_indicator(
&self,
conversation_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let activity = TeamsActivity {
activity_type: "typing".to_string(),
..Default::default()
};
self.send_teams_message(conversation_id, activity).await?;
Ok(())
}
pub async fn get_conversation_members(
&self,
conversation_id: &str,
) -> Result<Vec<TeamsMember>, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let token = self.get_access_token().await?;
let url = format!(
"{}/v3/conversations/{}/members",
self.service_url, conversation_id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?;
if response.status().is_success() {
let members: Vec<TeamsMember> = response.json().await?;
Ok(members)
} else {
let error_text = response.text().await?;
Err(format!("Failed to get conversation members: {}", error_text).into())
}
}
}
#[async_trait]
@ -244,14 +345,20 @@ impl ChannelAdapter for TeamsAdapter {
return Err("Teams not configured".into());
}
// In Teams, user_id is typically the conversation ID
let message_id = self
.send_teams_message(&response.user_id, None, &response.content)
.await?;
// Try to use existing conversation or create a new one
let conversation_id = self.create_conversation(&response.user_id).await?;
let activity = TeamsActivity {
activity_type: "message".to_string(),
text: Some(response.content.clone()),
..Default::default()
};
let message_id = self.send_teams_message(&conversation_id, activity).await?;
info!(
"Teams message sent to conversation {}: {} (message_id: {})",
response.user_id, response.content, message_id
conversation_id, response.content, message_id
);
Ok(())
@ -261,50 +368,49 @@ impl ChannelAdapter for TeamsAdapter {
&self,
payload: serde_json::Value,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
// Parse Teams activity payload
if let Some(activity_type) = payload["type"].as_str() {
match activity_type {
"message" => {
return Ok(payload["text"].as_str().map(|s| s.to_string()));
}
"invoke" => {
// Handle Teams-specific invokes (like adaptive card actions)
if let Some(name) = payload["name"].as_str() {
return Ok(Some(format!("Teams invoke: {}", name)));
// Parse Teams activity
let activity_type = payload["type"].as_str().unwrap_or("");
match activity_type {
"message" => {
if let Some(text) = payload["text"].as_str() {
// Remove bot mention if present
let cleaned_text = text
.replace(&format!("<at>{}</at>", self.bot_id), "")
.trim()
.to_string();
Ok(Some(cleaned_text))
} else if let Some(attachments) = payload["attachments"].as_array() {
if let Some(first_attachment) = attachments.first() {
let content_type = first_attachment["contentType"]
.as_str()
.unwrap_or("unknown");
Ok(Some(format!("Received attachment: {}", content_type)))
} else {
Ok(None)
}
}
_ => {
return Ok(None);
} else {
Ok(None)
}
}
"messageReaction" => {
let reaction_type = payload["reactionsAdded"]
.as_array()
.and_then(|r| r.first())
.and_then(|r| r["type"].as_str())
.unwrap_or("unknown");
Ok(Some(format!("Reaction: {}", reaction_type)))
}
_ => Ok(None),
}
Ok(None)
}
async fn get_user_info(
&self,
user_id: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
let token = self.get_access_token().await?;
let client = reqwest::Client::new();
// In Teams, user_id might be in format "29:1xyz..."
let url = format!("{}/v3/conversations/{}/members", self.service_url, user_id);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?;
if response.status().is_success() {
let members: Vec<serde_json::Value> = response.json().await?;
if let Some(first_member) = members.first() {
return Ok(first_member.clone());
}
}
// Teams user info would require Graph API access
// For now, return basic info
Ok(serde_json::json!({
"id": user_id,
"platform": "teams"
@ -312,16 +418,61 @@ impl ChannelAdapter for TeamsAdapter {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct TeamsActivity {
#[serde(rename = "type")]
pub activity_type: String,
pub text: String,
pub from: TeamsChannelAccount,
pub conversation: TeamsConversationAccount,
pub recipient: Option<TeamsChannelAccount>,
pub text: Option<String>,
pub attachments: Option<Vec<TeamsAttachment>>,
pub entities: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_actions: Option<TeamsSuggestedActions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_data: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsAttachment {
#[serde(rename = "contentType")]
pub content_type: String,
pub content: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsSuggestedActions {
pub actions: Vec<TeamsCardAction>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsCardAction {
#[serde(rename = "type")]
pub action_type: String,
pub title: String,
pub value: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsHeroCard {
pub title: Option<String>,
pub subtitle: Option<String>,
pub text: Option<String>,
pub images: Vec<TeamsCardImage>,
pub buttons: Option<Vec<TeamsCardAction>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsCardImage {
pub url: String,
pub alt: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsMember {
pub id: String,
pub name: Option<String>,
#[serde(rename = "userPrincipalName")]
pub user_principal_name: Option<String>,
pub role: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -333,38 +484,99 @@ pub struct TeamsChannelAccount {
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsConversationAccount {
pub id: String,
pub name: Option<String>,
#[serde(rename = "conversationType")]
pub conversation_type: Option<String>,
#[serde(rename = "tenantId")]
pub tenant_id: Option<String>,
#[serde(rename = "isGroup")]
pub is_group: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsAttachment {
#[serde(rename = "contentType")]
pub content_type: String,
pub content: serde_json::Value,
// Helper functions for Teams-specific features
pub fn create_adaptive_card(
title: &str,
body: Vec<serde_json::Value>,
actions: Vec<serde_json::Value>,
) -> serde_json::Value {
let mut all_body_items = vec![serde_json::json!({
"type": "TextBlock",
"text": title,
"weight": "Bolder",
"size": "Medium"
})];
all_body_items.extend(body);
serde_json::json!({
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.3",
"body": all_body_items,
"actions": actions
})
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamsAdaptiveCard {
#[serde(rename = "$schema")]
pub schema: String,
#[serde(rename = "type")]
pub card_type: String,
pub version: String,
pub body: Vec<serde_json::Value>,
pub actions: Option<Vec<serde_json::Value>>,
}
pub fn create_thumbnail_card(
title: &str,
subtitle: Option<&str>,
text: Option<&str>,
image_url: Option<&str>,
buttons: Vec<(&str, &str, &str)>,
) -> serde_json::Value {
let mut card = serde_json::json!({
"title": title
});
impl Default for TeamsAdaptiveCard {
fn default() -> Self {
Self {
schema: "http://adaptivecards.io/schemas/adaptive-card.json".to_string(),
card_type: "AdaptiveCard".to_string(),
version: "1.4".to_string(),
body: Vec::new(),
actions: None,
}
if let Some(sub) = subtitle {
card["subtitle"] = serde_json::Value::String(sub.to_string());
}
if let Some(txt) = text {
card["text"] = serde_json::Value::String(txt.to_string());
}
if let Some(img) = image_url {
card["images"] = serde_json::json!([{
"url": img
}]);
}
let button_list: Vec<serde_json::Value> = buttons
.into_iter()
.map(|(action_type, title, value)| {
serde_json::json!({
"type": action_type,
"title": title,
"value": value
})
})
.collect();
if !button_list.is_empty() {
card["buttons"] = serde_json::Value::Array(button_list);
}
card
}
pub fn create_message_with_mentions(
text: &str,
mentions: Vec<(&str, &str)>,
) -> (String, Vec<serde_json::Value>) {
let mut message = text.to_string();
let mention_entities: Vec<serde_json::Value> = mentions
.into_iter()
.map(|(user_id, display_name)| {
let mention_text = format!("<at>{}</at>", display_name);
message = message.replace(&format!("@{}", display_name), &mention_text);
serde_json::json!({
"type": "mention",
"mentioned": {
"id": user_id,
"name": display_name
},
"text": mention_text
})
})
.collect();
(message, mention_entities)
}

View file

@ -1,30 +1,49 @@
use async_trait::async_trait;
use log::{error, info};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Unused import
use uuid::Uuid;
use crate::core::bot::channels::ChannelAdapter;
use crate::core::config::ConfigManager;
use crate::shared::models::BotResponse;
use crate::shared::utils::DbPool;
#[derive(Debug)]
pub struct WhatsAppAdapter {
api_key: String,
phone_number_id: String,
webhook_verify_token: String,
#[allow(dead_code)]
business_account_id: String,
api_version: String,
}
impl WhatsAppAdapter {
pub fn new() -> Self {
// Load from environment variables (would be from config.csv in production)
let api_key = std::env::var("WHATSAPP_API_KEY").unwrap_or_default();
let phone_number_id = std::env::var("WHATSAPP_PHONE_NUMBER_ID").unwrap_or_default();
let webhook_verify_token =
std::env::var("WHATSAPP_VERIFY_TOKEN").unwrap_or_else(|_| "webhook_verify".to_string());
let business_account_id = std::env::var("WHATSAPP_BUSINESS_ACCOUNT_ID").unwrap_or_default();
let api_version = "v17.0".to_string();
pub fn new(pool: DbPool, bot_id: Uuid) -> Self {
let config_manager = ConfigManager::new(pool);
// Load from bot_configuration table with fallback to environment variables
let api_key = config_manager
.get_config(&bot_id, "whatsapp-api-key", None)
.unwrap_or_else(|_| std::env::var("WHATSAPP_API_KEY").unwrap_or_default());
let phone_number_id = config_manager
.get_config(&bot_id, "whatsapp-phone-number-id", None)
.unwrap_or_else(|_| std::env::var("WHATSAPP_PHONE_NUMBER_ID").unwrap_or_default());
let webhook_verify_token = config_manager
.get_config(&bot_id, "whatsapp-verify-token", Some("webhook_verify"))
.unwrap_or_else(|_| {
std::env::var("WHATSAPP_VERIFY_TOKEN")
.unwrap_or_else(|_| "webhook_verify".to_string())
});
let business_account_id = config_manager
.get_config(&bot_id, "whatsapp-business-account-id", None)
.unwrap_or_else(|_| std::env::var("WHATSAPP_BUSINESS_ACCOUNT_ID").unwrap_or_default());
let api_version = config_manager
.get_config(&bot_id, "whatsapp-api-version", Some("v17.0"))
.unwrap_or_else(|_| "v17.0".to_string());
Self {
api_key,
@ -49,11 +68,9 @@ impl WhatsAppAdapter {
let payload = serde_json::json!({
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": to,
"type": "text",
"text": {
"preview_url": false,
"body": message
}
});
@ -83,7 +100,7 @@ impl WhatsAppAdapter {
to: &str,
template_name: &str,
language_code: &str,
parameters: Vec<String>,
components: Vec<serde_json::Value>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
@ -92,21 +109,7 @@ impl WhatsAppAdapter {
self.api_version, self.phone_number_id
);
let components = if !parameters.is_empty() {
vec![serde_json::json!({
"type": "body",
"parameters": parameters.iter().map(|p| {
serde_json::json!({
"type": "text",
"text": p
})
}).collect::<Vec<_>>()
})]
} else {
vec![]
};
let payload = serde_json::json!({
let mut payload = serde_json::json!({
"messaging_product": "whatsapp",
"to": to,
"type": "template",
@ -114,11 +117,14 @@ impl WhatsAppAdapter {
"name": template_name,
"language": {
"code": language_code
},
"components": components
}
}
});
if !components.is_empty() {
payload["template"]["components"] = serde_json::Value::Array(components);
}
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
@ -142,8 +148,8 @@ impl WhatsAppAdapter {
pub async fn send_media_message(
&self,
to: &str,
media_type: &str,
media_url: &str,
media_type: &str,
caption: Option<&str>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
@ -153,13 +159,41 @@ impl WhatsAppAdapter {
self.api_version, self.phone_number_id
);
let mut media_object = serde_json::json!({
"link": media_url
});
if let Some(caption_text) = caption {
media_object["caption"] = serde_json::json!(caption_text);
}
let media_object = match media_type {
"image" => {
let mut obj = serde_json::json!({
"link": media_url
});
if let Some(cap) = caption {
obj["caption"] = serde_json::Value::String(cap.to_string());
}
obj
}
"video" => {
let mut obj = serde_json::json!({
"link": media_url
});
if let Some(cap) = caption {
obj["caption"] = serde_json::Value::String(cap.to_string());
}
obj
}
"audio" => serde_json::json!({
"link": media_url
}),
"document" => {
let mut obj = serde_json::json!({
"link": media_url
});
if let Some(cap) = caption {
obj["filename"] = serde_json::Value::String(cap.to_string());
}
obj
}
_ => serde_json::json!({
"link": media_url
}),
};
let payload = serde_json::json!({
"messaging_product": "whatsapp",
@ -188,51 +222,123 @@ impl WhatsAppAdapter {
}
}
pub fn verify_webhook(&self, token: &str) -> bool {
token == self.webhook_verify_token
}
/// Create a new message template in the business account
pub async fn create_message_template(
pub async fn send_location_message(
&self,
template_name: &str,
template_category: &str,
template_body: &str,
to: &str,
latitude: f64,
longitude: f64,
name: Option<&str>,
address: Option<&str>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/message_templates",
self.api_version, self.business_account_id
"https://graph.facebook.com/{}/{}/messages",
self.api_version, self.phone_number_id
);
let mut location = serde_json::json!({
"latitude": latitude,
"longitude": longitude
});
if let Some(n) = name {
location["name"] = serde_json::Value::String(n.to_string());
}
if let Some(a) = address {
location["address"] = serde_json::Value::String(a.to_string());
}
let payload = serde_json::json!({
"name": template_name,
"category": template_category,
"language": "en",
"components": [{
"type": "BODY",
"text": template_body
}]
"messaging_product": "whatsapp",
"to": to,
"type": "location",
"location": location
});
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await?;
if response.status().is_success() {
let result: serde_json::Value = response.json().await?;
Ok(result["id"].as_str().unwrap_or("").to_string())
Ok(result["messages"][0]["id"]
.as_str()
.unwrap_or("")
.to_string())
} else {
let error_text = response.text().await?;
Err(format!("Failed to create template: {}", error_text).into())
Err(format!("WhatsApp API error: {}", error_text).into())
}
}
pub async fn mark_message_as_read(
&self,
message_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/messages",
self.api_version, self.phone_number_id
);
let payload = serde_json::json!({
"messaging_product": "whatsapp",
"status": "read",
"message_id": message_id
});
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Failed to mark message as read: {}", error_text).into());
}
Ok(())
}
pub async fn get_business_profile(&self) -> Result<WhatsAppBusinessProfile, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/whatsapp_business_profile",
self.api_version, self.phone_number_id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.query(&[("fields", "about,address,description,email,profile_picture_url,websites,vertical")])
.send()
.await?;
if response.status().is_success() {
let profiles: serde_json::Value = response.json().await?;
if let Some(data) = profiles["data"].as_array() {
if let Some(first_profile) = data.first() {
let profile: WhatsAppBusinessProfile = serde_json::from_value(first_profile.clone())?;
return Ok(profile);
}
}
Err("No business profile found".into())
} else {
let error_text = response.text().await?;
Err(format!("Failed to get business profile: {}", error_text).into())
}
}
/// Upload media to WhatsApp Business API
pub async fn upload_media(
&self,
file_path: &str,
@ -242,16 +348,23 @@ impl WhatsAppAdapter {
let url = format!(
"https://graph.facebook.com/{}/{}/media",
self.api_version, self.business_account_id
self.api_version, self.phone_number_id
);
let file = tokio::fs::read(file_path).await?;
let file_data = tokio::fs::read(file_path).await?;
let part = reqwest::multipart::Part::bytes(file_data)
.mime_str(mime_type)?
.file_name(file_path.to_string());
let form = reqwest::multipart::Form::new()
.part("file", part)
.text("messaging_product", "whatsapp");
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", mime_type)
.body(file)
.multipart(form)
.send()
.await?;
@ -264,90 +377,20 @@ impl WhatsAppAdapter {
}
}
/// Get business profile information
pub async fn get_business_profile(
&self,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/whatsapp_business_profile",
self.api_version, self.business_account_id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.query(&[(
"fields",
"about,address,description,email,profile_picture_url,websites",
)])
.send()
.await?;
if response.status().is_success() {
Ok(response.json().await?)
} else {
let error_text = response.text().await?;
Err(format!("Failed to get business profile: {}", error_text).into())
}
pub fn verify_webhook(&self, token: &str) -> bool {
token == self.webhook_verify_token
}
/// Update business profile
pub async fn update_business_profile(
pub async fn handle_webhook_verification(
&self,
profile_data: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/whatsapp_business_profile",
self.api_version, self.business_account_id
);
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&profile_data)
.send()
.await?;
if response.status().is_success() {
Ok(())
mode: &str,
token: &str,
challenge: &str,
) -> Option<String> {
if mode == "subscribe" && self.verify_webhook(token) {
Some(challenge.to_string())
} else {
let error_text = response.text().await?;
Err(format!("Failed to update business profile: {}", error_text).into())
}
}
/// Get message template analytics
pub async fn get_template_analytics(
&self,
template_name: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}/template_analytics",
self.api_version, self.business_account_id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.query(&[
("template_name", template_name),
("start", "30_days_ago"),
("end", "now"),
])
.send()
.await?;
if response.status().is_success() {
Ok(response.json().await?)
} else {
let error_text = response.text().await?;
Err(format!("Failed to get template analytics: {}", error_text).into())
None
}
}
}
@ -394,27 +437,43 @@ impl ChannelAdapter for WhatsAppAdapter {
if let Some(first_change) = changes.first() {
if let Some(messages) = first_change["value"]["messages"].as_array() {
if let Some(first_message) = messages.first() {
let message_type = first_message["type"].as_str().unwrap_or("");
match message_type {
"text" => {
return Ok(first_message["text"]["body"]
.as_str()
.map(|s| s.to_string()));
}
"image" | "document" | "audio" | "video" => {
return Ok(Some(format!(
"Received {} message",
message_type
)));
}
_ => {
return Ok(Some(format!(
"Received unsupported message type: {}",
message_type
)));
}
// Mark message as read
if let Some(message_id) = first_message["id"].as_str() {
let _ = self.mark_message_as_read(message_id).await;
}
// Extract message content based on type
let message_type = first_message["type"].as_str().unwrap_or("unknown");
return match message_type {
"text" => {
Ok(first_message["text"]["body"].as_str().map(|s| s.to_string()))
}
"image" | "video" | "audio" | "document" => {
let caption = first_message[message_type]["caption"]
.as_str()
.unwrap_or("");
Ok(Some(format!("Received {} with caption: {}", message_type, caption)))
}
"location" => {
let lat = first_message["location"]["latitude"].as_f64().unwrap_or(0.0);
let lon = first_message["location"]["longitude"].as_f64().unwrap_or(0.0);
Ok(Some(format!("Location: {}, {}", lat, lon)))
}
"button" => {
Ok(first_message["button"]["text"].as_str().map(|s| s.to_string()))
}
"interactive" => {
if let Some(button_reply) = first_message["interactive"]["button_reply"].as_object() {
Ok(button_reply["id"].as_str().map(|s| s.to_string()))
} else if let Some(list_reply) = first_message["interactive"]["list_reply"].as_object() {
Ok(list_reply["id"].as_str().map(|s| s.to_string()))
} else {
Ok(None)
}
}
_ => Ok(None)
};
}
}
}
@ -429,32 +488,29 @@ impl ChannelAdapter for WhatsAppAdapter {
&self,
user_id: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::new();
let url = format!(
"https://graph.facebook.com/{}/{}",
self.api_version, user_id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.send()
.await?;
if response.status().is_success() {
Ok(response.json().await?)
} else {
Ok(serde_json::json!({
"id": user_id,
"platform": "whatsapp"
}))
}
// WhatsApp doesn't provide user profile info via API for privacy reasons
// Return basic info
Ok(serde_json::json!({
"id": user_id,
"platform": "whatsapp"
}))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppBusinessProfile {
pub about: Option<String>,
pub address: Option<String>,
pub description: Option<String>,
pub email: Option<String>,
pub profile_picture_url: Option<String>,
pub websites: Option<Vec<String>>,
pub vertical: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppWebhookPayload {
pub object: String,
pub entry: Vec<WhatsAppEntry>,
}
@ -474,6 +530,7 @@ pub struct WhatsAppChange {
pub struct WhatsAppValue {
pub messaging_product: String,
pub metadata: WhatsAppMetadata,
pub contacts: Option<Vec<WhatsAppContact>>,
pub messages: Option<Vec<WhatsAppMessage>>,
pub statuses: Option<Vec<WhatsAppStatus>>,
}
@ -484,6 +541,17 @@ pub struct WhatsAppMetadata {
pub phone_number_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppContact {
pub profile: WhatsAppProfile,
pub wa_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppProfile {
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppMessage {
pub from: String,
@ -492,6 +560,13 @@ pub struct WhatsAppMessage {
#[serde(rename = "type")]
pub message_type: String,
pub text: Option<WhatsAppText>,
pub image: Option<WhatsAppMedia>,
pub video: Option<WhatsAppMedia>,
pub audio: Option<WhatsAppMedia>,
pub document: Option<WhatsAppMedia>,
pub location: Option<WhatsAppLocation>,
pub button: Option<WhatsAppButton>,
pub interactive: Option<WhatsAppInteractive>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -499,6 +574,49 @@ pub struct WhatsAppText {
pub body: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppMedia {
pub id: String,
pub mime_type: Option<String>,
pub sha256: Option<String>,
pub caption: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppLocation {
pub latitude: f64,
pub longitude: f64,
pub name: Option<String>,
pub address: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppButton {
pub text: String,
pub payload: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppInteractive {
#[serde(rename = "type")]
pub interactive_type: String,
pub button_reply: Option<WhatsAppButtonReply>,
pub list_reply: Option<WhatsAppListReply>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppButtonReply {
pub id: String,
pub title: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppListReply {
pub id: String,
pub title: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhatsAppStatus {
pub id: String,
@ -506,3 +624,73 @@ pub struct WhatsAppStatus {
pub timestamp: String,
pub recipient_id: String,
}
// Helper functions for WhatsApp-specific features
pub fn create_interactive_buttons(text: &str, buttons: Vec<(&str, &str)>) -> serde_json::Value {
let button_list: Vec<serde_json::Value> = buttons
.into_iter()
.take(3) // WhatsApp limits to 3 buttons
.map(|(id, title)| {
serde_json::json!({
"type": "reply",
"reply": {
"id": id,
"title": title
}
})
})
.collect();
serde_json::json!({
"type": "button",
"body": {
"text": text
},
"action": {
"buttons": button_list
}
})
}
pub fn create_interactive_list(
text: &str,
button_text: &str,
sections: Vec<(String, Vec<(String, String, Option<String>)>)>,
) -> serde_json::Value {
let section_list: Vec<serde_json::Value> = sections
.into_iter()
.map(|(title, rows)| {
let row_list: Vec<serde_json::Value> = rows
.into_iter()
.take(10) // WhatsApp limits to 10 rows per section
.map(|(id, title, description)| {
let mut row = serde_json::json!({
"id": id,
"title": title
});
if let Some(desc) = description {
row["description"] = serde_json::Value::String(desc);
}
row
})
.collect();
serde_json::json!({
"title": title,
"rows": row_list
})
})
.collect();
serde_json::json!({
"type": "list",
"body": {
"text": text
},
"action": {
"button": button_text,
"sections": section_list
}
})
}

View file

@ -1399,7 +1399,7 @@ pub async fn recent_files(
if let Some(drive) = &state.drive {
let bucket = "drive";
for (path, _) in recent_files.iter().take(query.limit.unwrap_or(20)) {
for (path, _) in recent_files.iter().take(query.limit.unwrap_or(20) as usize) {
// TODO: Fix get_object_info API call
/*
if let Ok(object) = drive.get_object_info(bucket, path).await {
@ -1417,9 +1417,16 @@ pub async fn recent_files(
}
}
let total = items.len() as i64;
let limit = items.len() as i32;
return Ok(Json(ApiResponse {
success: true,
data: Some(ListResponse { items }),
data: Some(ListResponse {
files: items,
total,
offset: 0,
limit,
}),
message: None,
error: None,
}));
@ -1621,9 +1628,9 @@ pub async fn get_permissions(
Ok(Json(ApiResponse {
success: true,
data: Some(PermissionsResponse {
success: true,
path,
permissions,
inherited,
}),
message: None,
error: None,
@ -1772,15 +1779,13 @@ pub async fn get_shared(
}
shared_files.push(SharedFile {
id: share_id,
name: path.split('/').last().unwrap_or(&path).to_string(),
share_id,
path,
size,
modified,
shared_by: shared_by.unwrap_or_else(|| "Unknown".to_string()),
shared_at: created_at,
shared_with: vec![shared_by.unwrap_or_else(|| "Unknown".to_string())],
permissions,
created_at,
expires_at,
access_count: 0,
});
}
}

View file

@ -224,7 +224,7 @@ pub struct TaskEngine {
}
impl TaskEngine {
pub fn new(db: Arc<DbPool>) -> Self {
pub fn new(db: DbPool) -> Self {
Self {
db,
cache: Arc::new(RwLock::new(vec![])),
@ -265,61 +265,11 @@ impl TaskEngine {
Ok(task.into())
}
pub async fn update_task(
&self,
id: Uuid,
update: TaskUpdate,
) -> Result<TaskResponse, Box<dyn std::error::Error>> {
let mut cache = self.cache.write().await;
// Removed duplicate update_task - using database version below
if let Some(task) = cache.iter_mut().find(|t| t.id == id) {
if let Some(title) = update.title {
task.title = title;
}
if let Some(description) = update.description {
task.description = Some(description);
}
if let Some(status) = update.status {
task.status = status;
if task.status == "completed" || task.status == "done" {
task.completed_at = Some(Utc::now());
}
}
if let Some(priority) = update.priority {
task.priority = priority;
}
if let Some(assignee) = update.assignee {
task.assignee_id = Some(Uuid::parse_str(&assignee)?);
}
if let Some(due_date) = update.due_date {
task.due_date = Some(due_date);
}
if let Some(tags) = update.tags {
task.tags = tags;
}
task.updated_at = Utc::now();
// Removed duplicate delete_task - using database version below
Ok(task.clone().into())
} else {
Err("Task not found".into())
}
}
pub async fn delete_task(&self, id: Uuid) -> Result<(), Box<dyn std::error::Error>> {
let mut cache = self.cache.write().await;
cache.retain(|t| t.id != id);
Ok(())
}
pub async fn get_task(&self, id: Uuid) -> Result<TaskResponse, Box<dyn std::error::Error>> {
let cache = self.cache.read().await;
cache
.iter()
.find(|t| t.id == id)
.cloned()
.map(|t| t.into())
.ok_or_else(|| "Task not found".into())
}
// Removed duplicate get_task - using database version below
pub async fn list_tasks(
&self,
@ -359,22 +309,7 @@ impl TaskEngine {
Ok(tasks.into_iter().map(|t| t.into()).collect())
}
pub async fn assign_task(
&self,
id: Uuid,
assignee: String,
) -> Result<TaskResponse, Box<dyn std::error::Error>> {
let assignee_id = Uuid::parse_str(&assignee)?;
let mut cache = self.cache.write().await;
if let Some(task) = cache.iter_mut().find(|t| t.id == id) {
task.assignee_id = Some(assignee_id);
task.updated_at = Utc::now();
Ok(task.clone().into())
} else {
Err("Task not found".into())
}
}
// Removed duplicate - using database version below
pub async fn update_status(
&self,
@ -402,10 +337,7 @@ pub async fn handle_task_create(
State(state): State<Arc<AppState>>,
Json(payload): Json<CreateTaskRequest>,
) -> Result<Json<TaskResponse>, StatusCode> {
let task_engine = state
.task_engine
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let task_engine = &state.task_engine;
match task_engine.create_task(payload).await {
Ok(task) => Ok(Json(task)),
@ -421,13 +353,10 @@ pub async fn handle_task_update(
Path(id): Path<Uuid>,
Json(payload): Json<TaskUpdate>,
) -> Result<Json<TaskResponse>, StatusCode> {
let task_engine = state
.task_engine
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let task_engine = &state.task_engine;
match task_engine.update_task(id, payload).await {
Ok(task) => Ok(Json(task)),
Ok(task) => Ok(Json(task.into())),
Err(e) => {
log::error!("Failed to update task: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
@ -439,10 +368,7 @@ pub async fn handle_task_delete(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, StatusCode> {
let task_engine = state
.task_engine
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let task_engine = &state.task_engine;
match task_engine.delete_task(id).await {
Ok(_) => Ok(StatusCode::NO_CONTENT),
@ -457,13 +383,10 @@ pub async fn handle_task_get(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<TaskResponse>, StatusCode> {
let task_engine = state
.task_engine
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let task_engine = &state.task_engine;
match task_engine.get_task(id).await {
Ok(task) => Ok(Json(task)),
Ok(task) => Ok(Json(task.into())),
Err(e) => {
log::error!("Failed to get task: {}", e);
Err(StatusCode::NOT_FOUND)
@ -718,7 +641,9 @@ impl TaskEngine {
) -> Result<Task, Box<dyn std::error::Error>> {
// For subtasks, we store parent relationship separately
// or in a separate junction table
let created = self.create_task(subtask).await?;
// Use create_task_with_db which accepts and returns Task
let created = self.create_task_with_db(subtask).await?;
// Update parent's subtasks list
// TODO: Implement with Diesel
@ -749,6 +674,7 @@ impl TaskEngine {
for dep_id in task.dependencies {
if let Ok(dep_task) = self.get_task(dep_id).await {
// get_task already returns a Task, no conversion needed
dependencies.push(dep_task);
}
}
@ -892,7 +818,19 @@ impl TaskEngine {
completed_at: None,
};
let created = self.create_task(task).await?;
// Convert Task to CreateTaskRequest for create_task
let task_request = CreateTaskRequest {
title: task.title,
description: task.description,
assignee_id: task.assignee_id,
reporter_id: task.reporter_id,
project_id: task.project_id,
priority: Some(task.priority),
due_date: task.due_date,
tags: Some(task.tags),
estimated_hours: task.estimated_hours,
};
let created = self.create_task(task_request).await?;
// Create checklist items
for item in template.checklist {
@ -922,7 +860,45 @@ impl TaskEngine {
*/
}
Ok(created)
// Convert TaskResponse to Task
let task = Task {
id: created.id,
title: created.title,
description: created.description,
status: match created.status {
TaskStatus::Todo => "todo".to_string(),
TaskStatus::InProgress => "in_progress".to_string(),
TaskStatus::Completed => "completed".to_string(),
TaskStatus::OnHold => "on_hold".to_string(),
TaskStatus::Review => "review".to_string(),
TaskStatus::Blocked => "blocked".to_string(),
TaskStatus::Cancelled => "cancelled".to_string(),
TaskStatus::Done => "done".to_string(),
},
priority: match created.priority {
TaskPriority::Low => "low".to_string(),
TaskPriority::Medium => "medium".to_string(),
TaskPriority::High => "high".to_string(),
TaskPriority::Urgent => "urgent".to_string(),
},
assignee_id: created.assignee.and_then(|a| Uuid::parse_str(&a).ok()),
reporter_id: if created.reporter == "system" {
None
} else {
Uuid::parse_str(&created.reporter).ok()
},
project_id: None,
tags: created.tags,
dependencies: created.dependencies,
due_date: created.due_date,
estimated_hours: created.estimated_hours,
actual_hours: created.actual_hours,
progress: created.progress,
created_at: created.created_at,
updated_at: created.updated_at,
completed_at: created.completed_at,
};
Ok(task)
}
/// Send notification to assignee
async fn notify_assignee(