Skip to main content

bot/
bot.rs

1use fluxer::prelude::*;
2use std::time::Instant;
3
4const PREFIX: &str = "!";
5
6struct Handler;
7
8fn parse_command(content: &str) -> Option<(&str, &str)> {
9    let trimmed = content.strip_prefix(PREFIX)?;
10    match trimmed.find(' ') {
11        Some(pos) => Some((&trimmed[..pos], trimmed[pos + 1..].trim())),
12        None => Some((trimmed, "")),
13    }
14}
15
16#[async_trait]
17impl EventHandler for Handler {
18    async fn on_ready(&self, ctx: Context, ready: Ready) {
19        println!("Logged in as {}", ready.user.username);
20        println!("current_user from cache: {:?}", ctx.cache.current_user().await);
21    }
22
23    async fn on_guild_create(&self, ctx: Context, guild: Guild) {
24        println!("GUILD_CREATE: {} (cache size: {})", guild.id, ctx.cache.guild_count().await);
25    }
26
27    async fn on_message(&self, ctx: Context, msg: Message) {
28        let user_cached = ctx.cache.user(&msg.author.id).await.is_some();
29        let ch_cached   = ctx.cache.channel(msg.channel_id.as_deref().unwrap_or("")).await.is_some();
30        let content_preview = msg.content.as_deref().unwrap_or("").chars().take(60).collect::<String>();
31        let attachments = msg.attachments.as_ref().map(|a| a.len()).unwrap_or(0);
32        let embeds = msg.embeds.as_ref().map(|e| e.len()).unwrap_or(0);
33        println!(
34            "[msg] author={}#{} channel={} guild={} | \"{}\" | attach={} embeds={} | cache: user={} ch={}",
35            msg.author.username,
36            msg.author.discriminator.as_deref().unwrap_or("0"),
37            msg.channel_id.as_deref().unwrap_or("?"),
38            msg.guild_id.as_deref().unwrap_or("DM"),
39            content_preview,
40            attachments,
41            embeds,
42            user_cached,
43            ch_cached,
44        );
45
46        if msg.author.bot.unwrap_or(false) {
47            return;
48        }
49
50        let content = match msg.content.as_deref() {
51            Some(c) => c,
52            None => return,
53        };
54
55        let channel_id = msg.channel_id.as_deref().unwrap_or_default();
56
57        let (cmd, args) = match parse_command(content) {
58            Some(v) => v,
59            None => return,
60        };
61
62        match cmd {
63            "ping" => {
64                let start = Instant::now();
65                let sent = ctx.http.send_message(channel_id, "Pong!").await;
66                let elapsed = start.elapsed().as_millis();
67
68                if let Ok(sent) = sent {
69                    let _ = ctx.http.edit_message(
70                        channel_id,
71                        &sent.id,
72                        &format!("Pong! {}ms", elapsed),
73                    ).await;
74                }
75            }
76
77            "say" => {
78                if args.is_empty() {
79                    let _ = ctx.http.send_message(channel_id, "Say what?").await;
80                    return;
81                }
82                let _ = ctx.http.delete_message(channel_id, &msg.id).await;
83                let _ = ctx.http.send_message(channel_id, args).await;
84            }
85
86            "embed" => {
87                let (title, desc) = match args.split_once('|') {
88                    Some((t, d)) => (t.trim(), d.trim()),
89                    None => {
90                        let _ = ctx.http.send_message(channel_id, "`!embed title | description`").await;
91                        return;
92                    }
93                };
94
95                let embed = EmbedBuilder::new()
96                    .title(title)
97                    .description(desc)
98                    .color(0x5865F2)
99                    .build();
100
101                let _ = ctx.http.send_embed(channel_id, None, vec![embed]).await;
102            }
103
104            "react" => {
105                let _ = ctx.http.add_reaction(channel_id, &msg.id, "❤️").await;
106            }
107
108            "purge" => {
109                let count: u8 = args.parse().unwrap_or(0);
110                if count == 0 || count > 100 {
111                    let _ = ctx.http.send_message(channel_id, "1-100.").await;
112                    return;
113                }
114
115                let query = GetMessagesQuery {
116                    limit: Some(count),
117                    ..Default::default()
118                };
119
120                if let Ok(messages) = ctx.http.get_messages(channel_id, query).await {
121                    let ids: Vec<&str> = messages.iter().map(|m| m.id.as_str()).collect();
122                    let _ = ctx.http.bulk_delete_messages(channel_id, ids).await;
123                }
124            }
125
126            "serverinfo" => {
127                let guild_id = match &msg.guild_id {
128                    Some(id) => id.as_str(),
129                    None => return,
130                };
131
132                if let Ok(guild) = ctx.http.get_guild(guild_id).await {
133                    let name = guild.name.as_deref().unwrap_or("Unknown");
134
135                    let members = ctx.http.get_guild_members(guild_id, Some(1000), None).await
136                        .map(|m| m.len().to_string())
137                        .unwrap_or("?".into());
138
139                    let embed = EmbedBuilder::new()
140                        .title(name)
141                        .field("Members", &members, true)
142                        .color(0x5865F2)
143                        .build();
144
145                    let _ = ctx.http.send_embed(channel_id, None, vec![embed]).await;
146                }
147            }
148
149            "attach" => {
150                if args.is_empty() {
151                    let _ = ctx.http.send_message(channel_id, "Usage: `!attach <file path>`").await;
152                    return;
153                }
154                let path = std::path::Path::new(args);
155                let filename = path
156                    .file_name()
157                    .and_then(|n| n.to_str())
158                    .unwrap_or("file")
159                    .to_string();
160                let content_type = match path.extension().and_then(|e| e.to_str()) {
161                    Some("mp3") => "audio/mpeg",
162                    Some("mp4") => "video/mp4",
163                    Some("mov") => "video/quicktime",
164                    Some("webm") => "video/webm",
165                    Some("png") => "image/png",
166                    Some("jpg") | Some("jpeg") => "image/jpeg",
167                    Some("gif") => "image/gif",
168                    Some("webp") => "image/webp",
169                    Some("txt") => "text/plain",
170                    Some("pdf") => "application/pdf",
171                    _ => "application/octet-stream",
172                };
173                match tokio::fs::read(path).await {
174                    Ok(data) => {
175                        let file = AttachmentFile {
176                            filename,
177                            data,
178                            content_type: Some(content_type.to_string()),
179                        };
180                        match ctx.http.send_files(channel_id, vec![file], None).await {
181                            Ok(msg) => println!("[attach] sent message {}", msg.id),
182                            Err(e) => eprintln!("[attach] error: {}", e),
183                        }
184                    }
185                    Err(e) => {
186                        eprintln!("[attach] failed to read file: {}", e);
187                        let _ = ctx.http.send_message(channel_id, &format!("Failed to read file: {}", e)).await;
188                    }
189                }
190            }
191
192            _ => {}
193        }
194    }
195}
196
197#[tokio::main]
198async fn main() {
199    let token = std::env::var("FLUXER_TOKEN")
200        .expect("Set FLUXER_TOKEN to your bot token");
201
202    let mut client = Client::builder(&token)
203        // .api_url("http://localhost:48763/api/v1") this is for self hosted instances
204        .event_handler(Handler)
205        .build();
206
207    if let Err(e) = client.start().await {
208        eprintln!("Error: {}", e);
209    }
210}