botserver/src/email/htmx.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

874 lines
28 KiB
Rust

use crate::core::shared::state::AppState;
use crate::core::config::EmailConfig;
use super::types::*;
use axum::{
extract::{Path, Query, State},
response::IntoResponse,
};
use diesel::prelude::*;
use log::{error, info, warn};
use mailparse::{parse_mail, MailHeaderMap};
use std::sync::Arc;
use uuid::Uuid;
fn extract_user_from_session(_state: &Arc<AppState>) -> Result<Uuid, String> {
Ok(Uuid::new_v4())
}
fn fetch_emails_from_folder(
config: &EmailConfig,
folder: &str,
) -> Result<Vec<EmailSummary>, String> {
#[cfg(feature = "mail")]
{
let client = imap::ClientBuilder::new(&config.server, config.port)
.connect()
.map_err(|e| format!("Connection error: {}", e))?;
let mut session = client
.login(&config.username, &config.password)
.map_err(|e| format!("Login failed: {:?}", e))?;
let folder_name = match folder {
"sent" => "Sent",
"drafts" => "Drafts",
"trash" => "Trash",
_ => "INBOX",
};
session
.select(folder_name)
.map_err(|e| format!("Select folder failed: {}", e))?;
let messages = session
.fetch("1:20", "(FLAGS RFC822.HEADER)")
.map_err(|e| format!("Fetch failed: {}", e))?;
let mut emails = Vec::new();
for message in messages.iter() {
if let Some(header) = message.header() {
let parsed = parse_mail(header).ok();
if let Some(mail) = parsed {
let subject = mail.headers.get_first_value("Subject").unwrap_or_default();
let from = mail.headers.get_first_value("From").unwrap_or_default();
let date = mail.headers.get_first_value("Date").unwrap_or_default();
let flags = message.flags();
let read = flags.iter().any(|f| matches!(f, imap::types::Flag::Seen));
let preview = subject.chars().take(100).collect();
emails.push(EmailSummary {
id: message.message.to_string(),
from_name: from.clone(),
from_email: from,
subject,
preview,
date,
read,
});
}
}
}
session.logout().ok();
Ok(emails)
}
#[cfg(not(feature = "mail"))]
{
Ok(Vec::new())
}
}
fn get_folder_counts(
config: &EmailConfig,
) -> Result<std::collections::HashMap<String, usize>, String> {
use std::collections::HashMap;
#[cfg(feature = "mail")]
{
let client = imap::ClientBuilder::new(&config.server, config.port)
.connect()
.map_err(|e| format!("Connection error: {}", e))?;
let mut session = client
.login(&config.username, &config.password)
.map_err(|e| format!("Login failed: {:?}", e))?;
let mut counts = HashMap::new();
for folder in ["INBOX", "Sent", "Drafts", "Trash"] {
if let Ok(mailbox) = session.examine(folder) {
counts.insert((*folder).to_string(), mailbox.exists as usize);
}
}
session.logout().ok();
Ok(counts)
}
#[cfg(not(feature = "mail"))]
{
Ok(HashMap::new())
}
}
fn fetch_email_by_id(config: &EmailConfig, id: &str) -> Result<EmailContent, String> {
#[cfg(feature = "mail")]
{
let client = imap::ClientBuilder::new(&config.server, config.port)
.connect()
.map_err(|e| format!("Connection error: {}", e))?;
let mut session = client
.login(&config.username, &config.password)
.map_err(|e| format!("Login failed: {:?}", e))?;
session
.select("INBOX")
.map_err(|e| format!("Select failed: {}", e))?;
let messages = session
.fetch(id, "RFC822")
.map_err(|e| format!("Fetch failed: {}", e))?;
if let Some(message) = messages.iter().next() {
if let Some(body) = message.body() {
let parsed = parse_mail(body).map_err(|e| format!("Parse failed: {}", e))?;
let subject = parsed
.headers
.get_first_value("Subject")
.unwrap_or_default();
let from = parsed.headers.get_first_value("From").unwrap_or_default();
let to = parsed.headers.get_first_value("To").unwrap_or_default();
let date = parsed.headers.get_first_value("Date").unwrap_or_default();
let body_text = parsed
.subparts
.iter()
.find_map(|p| p.get_body().ok())
.or_else(|| parsed.get_body().ok())
.unwrap_or_default();
session.logout().ok();
return Ok(EmailContent {
id: id.to_string(),
from_name: from.clone(),
from_email: from,
to,
subject,
body: body_text,
date,
read: false,
});
}
}
session.logout().ok();
Err("Email not found".to_string())
}
#[cfg(not(feature = "mail"))]
{
Err("Mail feature not enabled".to_string())
}
}
fn move_email_to_trash(config: &EmailConfig, id: &str) -> Result<(), String> {
#[cfg(feature = "mail")]
{
let client = imap::ClientBuilder::new(&config.server, config.port)
.connect()
.map_err(|e| format!("Connection error: {}", e))?;
let mut session = client
.login(&config.username, &config.password)
.map_err(|e| format!("Login failed: {:?}", e))?;
session
.select("INBOX")
.map_err(|e| format!("Select failed: {}", e))?;
session
.store(id, "+FLAGS (\\Deleted)")
.map_err(|e| format!("Store failed: {}", e))?;
session
.expunge()
.map_err(|e| format!("Expunge failed: {}", e))?;
session.logout().ok();
Ok(())
}
#[cfg(not(feature = "mail"))]
{
Err("Mail feature not enabled".to_string())
}
}
pub async fn list_emails_htmx(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> impl IntoResponse {
let folder = params
.get("folder")
.cloned()
.unwrap_or_else(|| "inbox".to_string());
let user_id = match extract_user_from_session(&state) {
Ok(id) => id,
Err(_) => {
return axum::response::Html(
r#"<div class="empty-state">
<h3>Authentication required</h3>
<p>Please sign in to view your emails</p>
</div>"#
.to_string(),
);
}
};
let conn = state.conn.clone();
let account_result = tokio::task::spawn_blocking(move || {
let db_conn_result = conn.get();
let mut db_conn = match db_conn_result {
Ok(c) => c,
Err(e) => return Err(format!("DB connection error: {}", e)),
};
diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1")
.bind::<diesel::sql_types::Uuid, _>(user_id)
.get_result::<EmailAccountRow>(&mut db_conn)
.optional()
.map_err(|e| format!("Failed to get email account: {}", e))
})
.await;
let account = match account_result {
Ok(Ok(Some(acc))) => acc,
Ok(Ok(None)) => {
return axum::response::Html(
r##"<div class="empty-state">
<h3>No email account configured</h3>
<p>Please add an email account in settings to get started</p>
<a href="#settings" class="btn-primary" style="margin-top: 1rem; display: inline-block;">Add Email Account</a>
</div>"##
.to_string(),
);
}
Ok(Err(e)) => {
error!("Email account query error: {}", e);
return axum::response::Html(
r#"<div class="empty-state">
<h3>Unable to load emails</h3>
<p>There was an error connecting to the database. Please try again later.</p>
</div>"#
.to_string(),
);
}
Err(e) => {
error!("Task join error: {}", e);
return axum::response::Html(
r#"<div class="empty-state">
<h3>Unable to load emails</h3>
<p>An internal error occurred. Please try again later.</p>
</div>"#
.to_string(),
);
}
};
let config = EmailConfig {
username: account.username.clone(),
password: account.password_encrypted.clone(),
server: account.imap_server.clone(),
port: account.imap_port as u16,
from: account.email.clone(),
smtp_server: account.smtp_server.clone(),
smtp_port: account.smtp_port as u16,
};
let emails = fetch_emails_from_folder(&config, &folder).unwrap_or_default();
let mut html = String::new();
use std::fmt::Write;
for email in &emails {
let unread_class = if !email.read { "unread" } else { "" };
let _ = write!(
html,
r##"<div class="mail-item {}"
hx-get="/api/email/{}"
hx-target="#mail-content"
hx-swap="innerHTML">
<div class="mail-header">
<span>{}</span>
<span class="text-sm text-gray">{}</span>
</div>
<div class="mail-subject">{}</div>
<div class="mail-preview">{}</div>
</div>"##,
unread_class, email.id, email.from_name, email.date, email.subject, email.preview
);
}
if html.is_empty() {
html = format!(
r#"<div class="empty-state">
<h3>No emails in {}</h3>
<p>This folder is empty</p>
</div>"#,
folder
);
}
axum::response::Html(html)
}
pub async fn list_folders_htmx(
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
let user_id = match extract_user_from_session(&state) {
Ok(id) => id,
Err(_) => {
return axum::response::Html(
r#"<div class="nav-item">Please sign in</div>"#.to_string(),
);
}
};
let conn = state.conn.clone();
let account_result = tokio::task::spawn_blocking(move || {
let db_conn_result = conn.get();
let mut db_conn = match db_conn_result {
Ok(c) => c,
Err(e) => return Err(format!("DB connection error: {}", e)),
};
diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1")
.bind::<diesel::sql_types::Uuid, _>(user_id)
.get_result::<EmailAccountRow>(&mut db_conn)
.optional()
.map_err(|e| format!("Failed to get email account: {}", e))
})
.await;
let account = match account_result {
Ok(Ok(Some(acc))) => acc,
Ok(Ok(None)) => {
return axum::response::Html(
r#"<div class="nav-item">No account configured</div>"#.to_string(),
);
}
Ok(Err(e)) => {
error!("Email folder query error: {}", e);
return axum::response::Html(
r#"<div class="nav-item">Error loading folders</div>"#.to_string(),
);
}
Err(e) => {
error!("Task join error: {}", e);
return axum::response::Html(
r#"<div class="nav-item">Error loading folders</div>"#.to_string(),
);
}
};
let config = EmailConfig {
username: account.username.clone(),
password: account.password_encrypted.clone(),
server: account.imap_server.clone(),
port: account.imap_port as u16,
from: account.email.clone(),
smtp_server: account.smtp_server.clone(),
smtp_port: account.smtp_port as u16,
};
let folder_counts = get_folder_counts(&config).unwrap_or_default();
let mut html = String::new();
for (folder_name, icon, count) in &[
("inbox", "", folder_counts.get("INBOX").unwrap_or(&0)),
("sent", "", folder_counts.get("Sent").unwrap_or(&0)),
("drafts", "", folder_counts.get("Drafts").unwrap_or(&0)),
("trash", "", folder_counts.get("Trash").unwrap_or(&0)),
] {
let active = if *folder_name == "inbox" {
"active"
} else {
""
};
let count_badge = if **count > 0 {
format!(
r#"<span style="margin-left: auto; font-size: 0.875rem; color: #64748b;">{}</span>"#,
count
)
} else {
String::new()
};
use std::fmt::Write;
let _ = write!(
html,
r##"<div class="nav-item {}"
hx-get="/api/email/list?folder={}"
hx-target="#mail-list"
hx-swap="innerHTML">
<span>{}</span> {}
{}
</div>"##,
active,
folder_name,
icon,
folder_name
.chars()
.next()
.unwrap_or_default()
.to_uppercase()
.collect::<String>()
+ &folder_name[1..],
count_badge
);
}
axum::response::Html(html)
}
pub async fn compose_email_htmx(
State(_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, EmailError> {
let html = r##"
<div class="mail-content-view">
<h2>Compose New Email</h2>
<form class="compose-form"
hx-post="/api/email/send"
hx-target="#mail-content"
hx-swap="innerHTML">
<div class="form-group">
<label>To:</label>
<input type="email" name="to" required>
</div>
<div class="form-group">
<label>Subject:</label>
<input type="text" name="subject" required>
</div>
<div class="form-group">
<label>Message:</label>
<textarea name="body" rows="10" required></textarea>
</div>
<div class="compose-actions">
<button type="submit" class="btn-primary">Send</button>
<button type="button" class="btn-secondary"
hx-post="/api/email/draft"
hx-include="closest form">Save Draft</button>
</div>
</form>
</div>
"##;
Ok(axum::response::Html(html))
}
pub async fn get_email_content_htmx(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, EmailError> {
let user_id = extract_user_from_session(&state)
.map_err(|_| EmailError("Authentication required".to_string()))?;
let conn = state.conn.clone();
let account = tokio::task::spawn_blocking(move || {
let mut db_conn = conn
.get()
.map_err(|e| format!("DB connection error: {}", e))?;
diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1")
.bind::<diesel::sql_types::Uuid, _>(user_id)
.get_result::<EmailAccountRow>(&mut db_conn)
.optional()
.map_err(|e| format!("Failed to get email account: {}", e))
})
.await
.map_err(|e| EmailError(format!("Task join error: {e}")))?
.map_err(EmailError)?;
let Some(account) = account else {
return Ok(axum::response::Html(
r#"<div class="mail-content-view">
<p>No email account configured</p>
</div>"#
.to_string(),
));
};
let config = EmailConfig {
username: account.username.clone(),
password: account.password_encrypted.clone(),
server: account.imap_server.clone(),
port: account.imap_port as u16,
from: account.email.clone(),
smtp_server: account.smtp_server.clone(),
smtp_port: account.smtp_port as u16,
};
let email_content = fetch_email_by_id(&config, &id)
.map_err(|e| EmailError(format!("Failed to fetch email: {}", e)))?;
let html = format!(
r##"
<div class="mail-content-view">
<div class="mail-actions">
<button hx-get="/api/email/compose?reply_to={}"
hx-target="#mail-content"
hx-swap="innerHTML">Reply</button>
<button hx-get="/api/email/compose?forward={}"
hx-target="#mail-content"
hx-swap="innerHTML">Forward</button>
<button hx-delete="/api/email/{}"
hx-target="#mail-list"
hx-swap="innerHTML"
hx-confirm="Delete this email?">Delete</button>
</div>
<h2>{}</h2>
<div style="display: flex; align-items: center; gap: 1rem; margin: 1rem 0;">
<div>
<div style="font-weight: 600;">{}</div>
<div class="text-sm text-gray">to: {}</div>
</div>
<div style="margin-left: auto;" class="text-sm text-gray">{}</div>
</div>
<div class="mail-body">
{}
</div>
</div>
"##,
id,
id,
id,
email_content.subject,
email_content.from_name,
email_content.to,
email_content.date,
email_content.body
);
Ok(axum::response::Html(html))
}
pub async fn delete_email_htmx(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> impl IntoResponse {
let user_id = match extract_user_from_session(&state) {
Ok(id) => id,
Err(_) => {
return axum::response::Html(
r#"<div class="empty-state">
<h3>Authentication required</h3>
<p>Please sign in to delete emails</p>
</div>"#
.to_string(),
);
}
};
let conn = state.conn.clone();
let account_result = tokio::task::spawn_blocking(move || {
let db_conn_result = conn.get();
let mut db_conn = match db_conn_result {
Ok(c) => c,
Err(e) => return Err(format!("DB connection error: {}", e)),
};
diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1")
.bind::<diesel::sql_types::Uuid, _>(user_id)
.get_result::<EmailAccountRow>(&mut db_conn)
.optional()
.map_err(|e| format!("Failed to get email account: {}", e))
})
.await;
let account = match account_result {
Ok(Ok(Some(acc))) => acc,
Ok(Ok(None)) => {
return axum::response::Html(
r#"<div class="empty-state">
<h3>No email account configured</h3>
<p>Please add an email account first</p>
</div>"#
.to_string(),
);
}
Ok(Err(e)) => {
error!("Email account query error: {}", e);
return axum::response::Html(
r#"<div class="empty-state">
<h3>Error deleting email</h3>
<p>Database error occurred</p>
</div>"#
.to_string(),
);
}
Err(e) => {
error!("Task join error: {}", e);
return axum::response::Html(
r#"<div class="empty-state">
<h3>Error deleting email</h3>
<p>An internal error occurred</p>
</div>"#
.to_string(),
);
}
};
let config = EmailConfig {
username: account.username.clone(),
password: account.password_encrypted.clone(),
server: account.imap_server.clone(),
port: account.imap_port as u16,
from: account.email.clone(),
smtp_server: account.smtp_server.clone(),
smtp_port: account.smtp_port as u16,
};
if let Err(e) = move_email_to_trash(&config, &id) {
error!("Failed to delete email: {}", e);
return axum::response::Html(
r#"<div class="empty-state">
<h3>Error deleting email</h3>
<p>Failed to move email to trash</p>
</div>"#
.to_string(),
);
}
info!("Email {} moved to trash", id);
axum::response::Html(
r#"<div class="success-message">
<p>Email moved to trash</p>
</div>
<script>
setTimeout(function() {
htmx.trigger('#mail-list', 'load');
}, 100);
</script>"#
.to_string(),
)
}
pub async fn list_labels_htmx(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
axum::response::Html(
r#"
<div class="label-item" style="--label-color: #ef4444;">
<span class="label-dot" style="background: #ef4444;"></span>
<span>Important</span>
</div>
<div class="label-item" style="--label-color: #3b82f6;">
<span class="label-dot" style="background: #3b82f6;"></span>
<span>Work</span>
</div>
<div class="label-item" style="--label-color: #22c55e;">
<span class="label-dot" style="background: #22c55e;"></span>
<span>Personal</span>
</div>
<div class="label-item" style="--label-color: #f59e0b;">
<span class="label-dot" style="background: #f59e0b;"></span>
<span>Finance</span>
</div>
"#
.to_string(),
)
}
pub async fn list_templates_htmx(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
axum::response::Html(
r#"
<div class="template-item" onclick="useTemplate('welcome')">
<h4>Welcome Email</h4>
<p>Standard welcome message for new contacts</p>
</div>
<div class="template-item" onclick="useTemplate('followup')">
<h4>Follow Up</h4>
<p>General follow-up template</p>
</div>
<div class="template-item" onclick="useTemplate('meeting')">
<h4>Meeting Request</h4>
<p>Request a meeting with scheduling options</p>
</div>
<p class="text-sm text-gray" style="margin-top: 1rem; text-align: center;">
Click a template to use it
</p>
"#
.to_string(),
)
}
pub async fn list_signatures_htmx(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
axum::response::Html(
r#"
<div class="signature-item" onclick="useSignature('default')">
<h4>Default Signature</h4>
<p class="text-sm text-gray">Best regards,<br>Your Name</p>
</div>
<div class="signature-item" onclick="useSignature('formal')">
<h4>Formal Signature</h4>
<p class="text-sm text-gray">Sincerely,<br>Your Name<br>Title | Company</p>
</div>
<p class="text-sm text-gray" style="margin-top: 1rem; text-align: center;">
Click a signature to insert it
</p>
"#
.to_string(),
)
}
pub async fn list_rules_htmx(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
axum::response::Html(
r#"
<div class="rule-item">
<div class="rule-header">
<span class="rule-name">Auto-archive newsletters</span>
<label class="toggle-label">
<input type="checkbox" checked>
<span class="toggle-switch"></span>
</label>
</div>
<p class="text-sm text-gray">From: *@newsletter.* → Archive</p>
</div>
<div class="rule-item">
<div class="rule-header">
<span class="rule-name">Label work emails</span>
<label class="toggle-label">
<input type="checkbox" checked>
<span class="toggle-switch"></span>
</label>
</div>
<p class="text-sm text-gray">From: *@company.com → Label: Work</p>
</div>
<button class="btn-secondary" style="width: 100%; margin-top: 1rem;">
+ Add New Rule
</button>
"#
.to_string(),
)
}
pub async fn search_emails_htmx(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> impl IntoResponse {
let query = params.get("q").map(|s| s.as_str()).unwrap_or("");
if query.is_empty() {
return axum::response::Html(
r#"
<div class="empty-state">
<p>Enter a search term to find emails</p>
</div>
"#
.to_string(),
);
}
let search_term = format!("%{query_lower}%", query_lower = query.to_lowercase());
let Ok(mut conn) = state.conn.get() else {
return axum::response::Html(
r#"
<div class="empty-state error">
<p>Database connection error</p>
</div>
"#
.to_string(),
);
};
let search_query = "SELECT id, subject, from_address, to_addresses, body_text, received_at
FROM emails
WHERE LOWER(subject) LIKE $1
OR LOWER(from_address) LIKE $1
OR LOWER(body_text) LIKE $1
ORDER BY received_at DESC
LIMIT 50";
let results: Vec<EmailSearchRow> = match diesel::sql_query(search_query)
.bind::<diesel::sql_types::Text, _>(&search_term)
.load::<EmailSearchRow>(&mut conn)
{
Ok(r) => r,
Err(e) => {
warn!("Email search query failed: {}", e);
Vec::new()
}
};
if results.is_empty() {
return axum::response::Html(format!(
r#"
<div class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<h3>No results for "{}"</h3>
<p>Try different keywords or check your spelling.</p>
</div>
"#,
query
));
}
let mut html = String::from(r#"<div class="search-results">"#);
use std::fmt::Write;
let _ = write!(
html,
r#"<div class="result-stats">Found {} results for "{}"</div>"#,
results.len(),
query
);
for row in results {
let preview = row
.body_text
.as_deref()
.unwrap_or("")
.chars()
.take(100)
.collect::<String>();
let formatted_date = row.received_at.format("%b %d, %Y").to_string();
let _ = write!(
html,
r##"
<div class="email-item" hx-get="/ui/mail/view/{}" hx-target="#email-content" hx-swap="innerHTML">
<div class="email-sender">{}</div>
<div class="email-subject">{}</div>
<div class="email-preview">{}</div>
<div class="email-date">{}</div>
</div>
"##,
row.id, row.from_address, row.subject, preview, formatted_date
);
}
html.push_str("</div>");
axum::response::Html(html)
}
pub async fn save_auto_responder(
State(_state): State<Arc<AppState>>,
axum::Form(form): axum::Form<std::collections::HashMap<String, String>>,
) -> impl IntoResponse {
info!("Saving auto-responder settings: {:?}", form);
axum::response::Html(
r#"
<div class="notification success">
Auto-responder settings saved successfully!
</div>
"#
.to_string(),
)
}