1use super::SendMessageRequest;
4use super::types::*;
5use super::user_handle::UserHandle;
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub struct MessageComposer {
12 drafts: HashMap<ChannelId, DraftMessage>,
14 mention_cache: Vec<UserHandle>,
16 emoji_shortcuts: HashMap<String, String>,
18}
19
20impl Default for MessageComposer {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl MessageComposer {
27 pub fn new() -> Self {
29 let mut emoji_shortcuts = HashMap::new();
30 emoji_shortcuts.insert(":)".to_string(), "😊".to_string());
31 emoji_shortcuts.insert(":D".to_string(), "😃".to_string());
32 emoji_shortcuts.insert(":((".to_string(), "😢".to_string());
33 emoji_shortcuts.insert("<3".to_string(), "❤️".to_string());
34 emoji_shortcuts.insert(":fire:".to_string(), "🔥".to_string());
35 emoji_shortcuts.insert(":rocket:".to_string(), "🚀".to_string());
36 emoji_shortcuts.insert(":+1:".to_string(), "👍".to_string());
37 emoji_shortcuts.insert(":-1:".to_string(), "👎".to_string());
38
39 Self {
40 drafts: HashMap::new(),
41 mention_cache: Vec::new(),
42 emoji_shortcuts,
43 }
44 }
45
46 pub fn start_draft(&mut self, channel_id: ChannelId) -> &mut DraftMessage {
48 self.drafts
49 .entry(channel_id)
50 .or_insert_with(|| DraftMessage::new(channel_id))
51 }
52
53 pub fn get_draft(&self, channel_id: ChannelId) -> Option<&DraftMessage> {
55 self.drafts.get(&channel_id)
56 }
57
58 pub fn update_draft(&mut self, channel_id: ChannelId, text: String) {
60 let draft = self.start_draft(channel_id);
61 draft.text = text;
62 draft.update_formatted();
63 }
64
65 pub fn add_mention(&mut self, channel_id: ChannelId, user: UserHandle) {
67 let draft = self.start_draft(channel_id);
68 draft.mentions.push(user.clone());
69
70 let mention_text = format!("@{} ", user);
72 draft.text.push_str(&mention_text);
73 draft.update_formatted();
74 }
75
76 pub fn add_attachment(&mut self, channel_id: ChannelId, attachment: DraftAttachment) {
78 let draft = self.start_draft(channel_id);
79 draft.attachments.push(attachment);
80 }
81
82 pub fn remove_attachment(&mut self, channel_id: ChannelId, index: usize) {
84 if let Some(draft) = self.drafts.get_mut(&channel_id)
85 && index < draft.attachments.len()
86 {
87 draft.attachments.remove(index);
88 }
89 }
90
91 pub fn set_reply_to(&mut self, channel_id: ChannelId, message_id: MessageId) {
93 let draft = self.start_draft(channel_id);
94 draft.reply_to = Some(message_id);
95 }
96
97 pub fn set_thread(&mut self, channel_id: ChannelId, thread_id: ThreadId) {
99 let draft = self.start_draft(channel_id);
100 draft.thread_id = Some(thread_id);
101 }
102
103 pub fn clear_draft(&mut self, channel_id: ChannelId) {
105 self.drafts.remove(&channel_id);
106 }
107
108 pub fn get_mention_suggestions(&self, partial: &str) -> Vec<UserHandle> {
110 self.mention_cache
111 .iter()
112 .filter(|user| {
113 user.as_str()
114 .to_lowercase()
115 .contains(&partial.to_lowercase())
116 })
117 .cloned()
118 .collect()
119 }
120
121 pub fn update_mention_cache(&mut self, users: Vec<UserHandle>) {
123 self.mention_cache = users;
124 }
125
126 pub fn apply_formatting(&mut self, channel_id: ChannelId, format: TextFormat) {
128 let draft = self.start_draft(channel_id);
129
130 match format {
131 TextFormat::Bold => {
132 draft.text = format!("**{}**", draft.text);
133 }
134 TextFormat::Italic => {
135 draft.text = format!("*{}*", draft.text);
136 }
137 TextFormat::Code => {
138 draft.text = format!("`{}`", draft.text);
139 }
140 TextFormat::Strike => {
141 draft.text = format!("~~{}~~", draft.text);
142 }
143 TextFormat::Quote => {
144 draft.text = format!("> {}", draft.text);
145 }
146 TextFormat::CodeBlock(lang) => {
147 draft.text = format!("```{}\n{}\n```", lang, draft.text);
148 }
149 }
150
151 draft.update_formatted();
152 }
153
154 pub fn insert_emoji(&mut self, channel_id: ChannelId, emoji: String) {
156 let draft = self.start_draft(channel_id);
157 draft.text.push_str(&emoji);
158 draft.update_formatted();
159 }
160
161 pub fn process_shortcuts(&mut self, channel_id: ChannelId) {
163 let shortcuts = self.emoji_shortcuts.clone();
165
166 let draft = self.start_draft(channel_id);
167
168 for (shortcut, emoji) in &shortcuts {
169 draft.text = draft.text.replace(shortcut, emoji);
170 }
171
172 draft.update_formatted();
173 }
174
175 pub fn validate_draft(&self, channel_id: ChannelId) -> Result<()> {
177 let draft = self
178 .drafts
179 .get(&channel_id)
180 .ok_or_else(|| anyhow::anyhow!("No draft found"))?;
181
182 if draft.text.trim().is_empty() && draft.attachments.is_empty() {
184 return Err(anyhow::anyhow!("Cannot send empty message"));
185 }
186
187 if draft.text.len() > 10000 {
189 return Err(anyhow::anyhow!("Message too long (max 10000 characters)"));
190 }
191
192 let total_size: usize = draft.attachments.iter().map(|a| a.size).sum();
194
195 if total_size > 100 * 1024 * 1024 {
196 return Err(anyhow::anyhow!("Total attachment size exceeds 100MB"));
197 }
198
199 Ok(())
200 }
201
202 pub fn build_message(&self, channel_id: ChannelId) -> Result<SendMessageRequest> {
204 let draft = self
205 .drafts
206 .get(&channel_id)
207 .ok_or_else(|| anyhow::anyhow!("No draft found"))?;
208
209 let content = if draft.formatted_text.is_some() {
211 MessageContent::RichText(MarkdownContent {
212 raw: draft.text.clone(),
213 formatted: draft.formatted_text.clone().unwrap_or_default(),
214 mentions: draft.mentions.clone(),
215 links: draft.extract_links(),
216 })
217 } else {
218 MessageContent::Text(draft.text.clone())
219 };
220
221 let attachments = draft.attachments.iter().map(|a| a.data.clone()).collect();
223
224 Ok(SendMessageRequest {
225 channel_id,
226 content,
227 attachments,
228 thread_id: draft.thread_id,
229 reply_to: draft.reply_to,
230 mentions: draft.mentions.clone(),
231 ephemeral: draft.ephemeral,
232 })
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct DraftMessage {
239 pub channel_id: ChannelId,
240 pub text: String,
241 pub formatted_text: Option<String>,
242 pub mentions: Vec<UserHandle>,
243 pub attachments: Vec<DraftAttachment>,
244 pub reply_to: Option<MessageId>,
245 pub thread_id: Option<ThreadId>,
246 pub ephemeral: bool,
247 pub created_at: chrono::DateTime<chrono::Utc>,
248 pub updated_at: chrono::DateTime<chrono::Utc>,
249}
250
251impl DraftMessage {
252 fn new(channel_id: ChannelId) -> Self {
254 let now = chrono::Utc::now();
255 Self {
256 channel_id,
257 text: String::new(),
258 formatted_text: None,
259 mentions: Vec::new(),
260 attachments: Vec::new(),
261 reply_to: None,
262 thread_id: None,
263 ephemeral: false,
264 created_at: now,
265 updated_at: now,
266 }
267 }
268
269 fn update_formatted(&mut self) {
271 if self.text.contains("**")
273 || self.text.contains("*")
274 || self.text.contains("`")
275 || self.text.contains("~~")
276 {
277 self.formatted_text = Some(self.text.clone());
278 }
279
280 self.updated_at = chrono::Utc::now();
281 }
282
283 fn extract_links(&self) -> Vec<String> {
285 fn try_re(p: &str) -> Option<regex::Regex> {
286 regex::Regex::new(p).ok()
287 }
288 let url_regex = try_re(r"https?://[^\s<]+[^<.,:;'!\?\s]")
289 .or_else(|| try_re(r"https?://.+"))
290 .or_else(|| try_re(r"https?://.*"));
291
292 if let Some(re) = url_regex {
293 re.find_iter(&self.text)
294 .map(|m| m.as_str().to_string())
295 .collect()
296 } else {
297 Vec::new()
298 }
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct DraftAttachment {
305 pub filename: String,
306 pub mime_type: String,
307 pub size: usize,
308 pub data: Vec<u8>,
309 pub thumbnail: Option<Vec<u8>>,
310}
311
312#[derive(Debug, Clone)]
314pub enum TextFormat {
315 Bold,
316 Italic,
317 Code,
318 Strike,
319 Quote,
320 CodeBlock(String),
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct AutocompleteSuggestion {
326 pub text: String,
327 pub icon: String,
328 pub description: String,
329 pub action: AutocompleteAction,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub enum AutocompleteAction {
335 InsertMention(UserHandle),
336 InsertEmoji(String),
337 InsertCommand(String),
338 InsertChannel(ChannelId),
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_draft_creation() {
347 let mut composer = MessageComposer::new();
348 let channel = ChannelId::new();
349
350 composer.update_draft(channel, "Hello world".to_string());
351
352 let draft = composer.get_draft(channel).unwrap();
353 assert_eq!(draft.text, "Hello world");
354 }
355
356 #[test]
357 fn test_mention_addition() {
358 let mut composer = MessageComposer::new();
359 let channel = ChannelId::new();
360 let user = UserHandle::from("alice");
361
362 composer.add_mention(channel, user.clone());
363
364 let draft = composer.get_draft(channel).unwrap();
365 assert!(draft.mentions.contains(&user));
366 assert!(draft.text.contains("@alice"));
367 }
368
369 #[test]
370 fn test_emoji_shortcuts() {
371 let mut composer = MessageComposer::new();
372 let channel = ChannelId::new();
373
374 composer.update_draft(channel, "Hello :) :fire:".to_string());
375 composer.process_shortcuts(channel);
376
377 let draft = composer.get_draft(channel).unwrap();
378 assert!(draft.text.contains("😊"));
379 assert!(draft.text.contains("🔥"));
380 }
381
382 #[test]
383 fn test_draft_validation() {
384 let mut composer = MessageComposer::new();
385 let channel = ChannelId::new();
386
387 let result = composer.validate_draft(channel);
389 assert!(result.is_err());
390
391 composer.update_draft(channel, "Valid message".to_string());
393 let result = composer.validate_draft(channel);
394 assert!(result.is_ok());
395 }
396}