Skip to content

Commit 0dce6e8

Browse files
committed
feat(status): add bases for ticket status updates
1 parent ce9cf89 commit 0dce6e8

15 files changed

Lines changed: 682 additions & 23 deletions

File tree

Cargo.lock

Lines changed: 341 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
22
members = ["rustmail", "rustmail_panel"]
3-
resolver = "3"
3+
resolver = "3"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Add migration script here
2+
CREATE TABLE IF NOT EXISTS "thread_status" (
3+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
4+
"thread_id" TEXT NOT NULL UNIQUE,
5+
"channel_id" INTEGER NOT NULL UNIQUE,
6+
"owner_id" TEXT NOT NULL,
7+
"taken_by" TEXT DEFAULT NULL,
8+
"last_message_by" TEXT NOT NULL,
9+
"last_message_at" INTEGER NOT NULL,
10+
CONSTRAINT "thread_status_thread_id_fkey" FOREIGN KEY ("thread_id") REFERENCES "threads" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
11+
);

rustmail/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ rand = "0.8.5"
2525
base64 = "0.22.1"
2626
subtle = "2.6.1"
2727
chrono-tz = "0.10.4"
28+
console-subscriber = "0.4.1"
2829

2930
[dependencies.uuid]
3031
version = "1.18.1"

rustmail/src/commands/anonreply/text_command/anonreply.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::prelude::db::*;
33
use crate::prelude::errors::*;
44
use crate::prelude::handlers::*;
55
use crate::prelude::utils::*;
6+
use crate::types::TicketAuthor;
7+
use chrono::Utc;
68
use serenity::all::{Context, GuildId, Message, UserId};
79
use std::collections::HashMap;
810
use std::sync::Arc;
@@ -66,6 +68,17 @@ pub async fn anonreply(
6668
.await
6769
.map_err(|_| validation_failed("Failed to allocate message number"))?;
6870

71+
let mut ticket_status = match get_thread_status(&thread.id, db_pool).await {
72+
Some(status) => status,
73+
None => {
74+
return Err(validation_failed("Failed to get thread status"));
75+
}
76+
};
77+
78+
ticket_status.last_message_by = TicketAuthor::Staff;
79+
ticket_status.last_message_at = Utc::now().timestamp();
80+
update_thread_status(&thread.id, &ticket_status, db_pool).await?;
81+
6982
let _ = msg.delete(&ctx.http).await;
7083

7184
let mut sr = MessageBuilder::begin_staff_reply(

rustmail/src/commands/reply/slash_command/reply.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use crate::prelude::db::*;
44
use crate::prelude::errors::*;
55
use crate::prelude::handlers::*;
66
use crate::prelude::i18n::*;
7+
use crate::prelude::types::*;
78
use crate::prelude::utils::*;
9+
use chrono::Utc;
810
use serenity::FutureExt;
911
use serenity::all::{
1012
Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context,
@@ -155,6 +157,17 @@ impl RegistrableCommand for ReplyCommand {
155157
.await
156158
.map_err(|_| validation_failed("Failed to allocate message number"))?;
157159

160+
let mut ticket_status = match get_thread_status(&thread.id, db_pool).await {
161+
Some(status) => status,
162+
None => {
163+
return Err(validation_failed("Failed to get thread status"));
164+
}
165+
};
166+
167+
ticket_status.last_message_by = TicketAuthor::Staff;
168+
ticket_status.last_message_at = Utc::now().timestamp();
169+
update_thread_status(&thread.id, &ticket_status, db_pool).await?;
170+
158171
let mut sr = MessageBuilder::begin_staff_reply(
159172
&ctx,
160173
&config,

rustmail/src/commands/reply/text_command/reply.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use crate::prelude::config::*;
22
use crate::prelude::db::*;
33
use crate::prelude::errors::*;
44
use crate::prelude::handlers::*;
5+
use crate::prelude::types::*;
56
use crate::prelude::utils::*;
7+
use chrono::Utc;
68
use serenity::all::{Context, GuildId, Message, UserId};
79
use std::collections::HashMap;
810
use std::sync::Arc;
@@ -38,6 +40,17 @@ pub async fn reply(
3840
.await
3941
.map_err(|_| validation_failed("Failed to allocate message number"))?;
4042

43+
let mut ticket_status = match get_thread_status(&thread.id, db_pool).await {
44+
Some(status) => status,
45+
None => {
46+
return Err(validation_failed("Failed to get thread status"));
47+
}
48+
};
49+
50+
ticket_status.last_message_by = TicketAuthor::Staff;
51+
ticket_status.last_message_at = Utc::now().timestamp();
52+
update_thread_status(&thread.id, &ticket_status, db_pool).await?;
53+
4154
let _ = msg.delete(&ctx.http).await;
4255

4356
let mut sr = MessageBuilder::begin_staff_reply(

rustmail/src/db/operations/threads.rs

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::prelude::db::*;
22
use crate::prelude::errors::*;
3+
use crate::prelude::types::*;
34
use chrono::Utc;
45
use serenity::all::{ChannelId, GuildChannel, UserId};
56
use sqlx::{Error, SqlitePool};
@@ -116,17 +117,17 @@ pub async fn create_thread_for_user(
116117
let channel_id = channel.id.to_string();
117118
let thread_id = Uuid::new_v4().to_string();
118119

119-
match sqlx::query!(
120+
let res = match sqlx::query!(
120121
"INSERT INTO threads (id, user_id, user_name, channel_id) VALUES (?, ?, ?, ?)",
121122
thread_id,
122123
user_id,
123124
user_name,
124125
channel_id
125126
)
126-
.execute(pool)
127+
.execute(&pool.clone())
127128
.await
128129
{
129-
Ok(_) => Ok(thread_id),
130+
Ok(_) => Ok(thread_id.clone()),
130131
Err(Error::Database(db_err))
131132
if db_err.code() == Some(std::borrow::Cow::Borrowed("2067")) =>
132133
{
@@ -142,7 +143,43 @@ pub async fn create_thread_for_user(
142143
}
143144
}
144145
Err(e) => Err(e),
145-
}
146+
};
147+
148+
let channel_id = channel.id.get() as i64;
149+
let user_id_str = user_id.to_string();
150+
let timestamp = Utc::now().timestamp();
151+
152+
let _ = match sqlx::query!(
153+
"INSERT INTO thread_status (thread_id, channel_id, owner_id, taken_by, last_message_by, last_message_at) VALUES (?, ?, ?, ?, ?, ?)",
154+
thread_id,
155+
channel_id,
156+
user_id_str,
157+
None::<String>,
158+
"user",
159+
timestamp
160+
)
161+
.execute(&pool.clone())
162+
.await
163+
{
164+
Ok(_) => Ok(thread_id),
165+
Err(Error::Database(db_err))
166+
if db_err.code() == Some(std::borrow::Cow::Borrowed("2067")) =>
167+
{
168+
if let Some(existing_thread_id) =
169+
sqlx::query_scalar("SELECT id FROM threads WHERE user_id = ? AND status = 1")
170+
.bind(user_id)
171+
.fetch_optional(pool)
172+
.await?
173+
{
174+
Ok(existing_thread_id)
175+
} else {
176+
Err(Error::Database(db_err))
177+
}
178+
}
179+
Err(e) => Err(e),
180+
};
181+
182+
res
146183
}
147184

148185
pub async fn close_thread(
@@ -379,3 +416,97 @@ pub async fn is_orphaned_thread_channel(
379416

380417
Ok(exists)
381418
}
419+
420+
pub async fn get_all_thread_status(pool: &SqlitePool) -> Vec<TicketState> {
421+
match sqlx::query!(
422+
r#"
423+
SELECT channel_id, owner_id, taken_by, last_message_by, last_message_at
424+
FROM thread_status
425+
"#
426+
)
427+
.fetch_all(pool)
428+
.await
429+
{
430+
Ok(rows) => rows
431+
.into_iter()
432+
.map(|r| TicketState {
433+
channel_id: r.channel_id,
434+
owner_id: r.owner_id,
435+
taken_by: r.taken_by,
436+
last_message_by: TicketAuthor::from_str(&r.last_message_by),
437+
last_message_at: r.last_message_at,
438+
})
439+
.collect(),
440+
Err(e) => {
441+
eprintln!("Database error getting thread statuses: {:?}", e);
442+
vec![]
443+
}
444+
}
445+
}
446+
447+
pub async fn get_thread_status(thread_id: &str, pool: &SqlitePool) -> Option<TicketState> {
448+
match sqlx::query!(
449+
r#"
450+
SELECT
451+
channel_id,
452+
owner_id,
453+
taken_by,
454+
last_message_by,
455+
last_message_at
456+
FROM thread_status
457+
WHERE thread_id = ?
458+
"#,
459+
thread_id
460+
)
461+
.fetch_optional(pool)
462+
.await
463+
{
464+
Ok(Some(row)) => Some(TicketState {
465+
channel_id: row.channel_id,
466+
owner_id: row.owner_id,
467+
taken_by: row.taken_by,
468+
last_message_by: TicketAuthor::from_str(&row.last_message_by),
469+
last_message_at: row.last_message_at,
470+
}),
471+
Ok(None) => None,
472+
Err(e) => {
473+
eprintln!(
474+
"⚠️ Database error getting thread status for id {}: {:?}",
475+
thread_id, e
476+
);
477+
None
478+
}
479+
}
480+
}
481+
482+
pub async fn update_thread_status(
483+
thread_id: &str,
484+
ticket: &TicketState,
485+
pool: &SqlitePool,
486+
) -> ModmailResult<()> {
487+
let last_message_by = format!("{:?}", ticket.last_message_by).to_lowercase();
488+
489+
println!(
490+
"Updating thread status for thread_id {}: taken_by={:?}, last_message_by={}, last_message_at={}",
491+
thread_id, ticket.taken_by, last_message_by, ticket.last_message_at
492+
);
493+
494+
sqlx::query!(
495+
r#"
496+
UPDATE thread_status
497+
SET
498+
taken_by = ?,
499+
last_message_by = ?,
500+
last_message_at = ?
501+
WHERE thread_id = ?
502+
"#,
503+
ticket.taken_by,
504+
last_message_by,
505+
ticket.last_message_at,
506+
thread_id
507+
)
508+
.execute(pool)
509+
.await?;
510+
511+
Ok(())
512+
}

rustmail/src/handlers/ready_handler.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
use crate::db::get_all_thread_status;
12
use crate::prelude::commands::*;
23
use crate::prelude::config::*;
34
use crate::prelude::features::*;
45
use crate::prelude::modules::*;
56
use serenity::all::{ActivityData, CreateCommand, GuildId};
7+
use serenity::futures::future::join_all;
68
use serenity::{
79
all::{Context, EventHandler, Ready},
810
async_trait,
911
};
12+
use sqlx::SqlitePool;
1013
use std::sync::Arc;
14+
use std::time::Duration;
1115
use tokio::sync::watch::Receiver;
16+
use tokio::time::interval;
1217

1318
#[derive(Clone)]
1419
pub struct ReadyHandler {
@@ -39,11 +44,14 @@ impl EventHandler for ReadyHandler {
3944
}
4045
};
4146

47+
let config = self.config.clone();
48+
4249
ctx.set_activity(Option::from(ActivityData::playing(&self.config.bot.status)));
4350

4451
tokio::spawn({
4552
let ctx = ctx.clone();
46-
let config = self.config.clone();
53+
let config = config.clone();
54+
4755
async move {
4856
let recovery_results = recover_missing_messages(&ctx, &config).await;
4957
send_recovery_summary(&ctx, &config, &recovery_results).await;
@@ -52,7 +60,9 @@ impl EventHandler for ReadyHandler {
5260
}
5361
});
5462

55-
load_reminders(&ctx, &self.config, pool, self.shutdown.clone()).await;
63+
load_reminders(&ctx, &self.config, &pool.clone(), self.shutdown.clone()).await;
64+
65+
update_threads_status(&ctx, &pool.clone());
5666

5767
let guild_id = GuildId::new(self.config.bot.get_staff_guild_id());
5868
let guild_id2 = GuildId::new(self.config.bot.get_community_guild_id());
@@ -82,3 +92,40 @@ impl EventHandler for ReadyHandler {
8292
}
8393
}
8494
}
95+
96+
fn update_threads_status(ctx: &Context, pool: &SqlitePool) {
97+
tokio::spawn({
98+
let ctx = ctx.clone();
99+
let pool = pool.clone();
100+
101+
async move {
102+
let mut interval = interval(Duration::from_secs(60 * 10));
103+
104+
loop {
105+
let tickets_status = get_all_thread_status(&pool).await;
106+
107+
let mut handles = Vec::new();
108+
for ticket in tickets_status.iter() {
109+
let ctx = ctx.clone();
110+
let ticket = ticket.clone();
111+
let handle = tokio::spawn(async move {
112+
println!("Update");
113+
if let Err(e) = update_thread_status(&ctx, &ticket).await {
114+
eprintln!(
115+
"Failed to update thread status for channel {}: {:?}",
116+
ticket.channel_id, e
117+
);
118+
}
119+
});
120+
handles.push(handle);
121+
}
122+
123+
join_all(handles).await;
124+
125+
println!("Updated {} ticket statuses", tickets_status.len());
126+
127+
interval.tick().await;
128+
}
129+
}
130+
});
131+
}

rustmail/src/modules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ pub mod message_recovery;
33
pub mod reminders;
44
pub mod scheduled_closures;
55
pub mod threads;
6+
pub mod threads_status;
67

78
pub use commands::*;
89
pub use message_recovery::*;
910
pub use reminders::*;
1011
pub use scheduled_closures::*;
1112
pub use threads::*;
13+
pub use threads_status::*;

0 commit comments

Comments
 (0)