silly goober bot
1use crate::types::Data;
2use poise::serenity_prelude::{Context, GuildId};
3use serde::Serialize;
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7use tokio::net::{TcpListener, TcpStream};
8
9#[derive(Serialize)]
10struct MemberInfo {
11 id: String,
12 username: String,
13 avatar_url: Option<String>,
14}
15
16#[derive(Serialize)]
17struct MembersResponse {
18 guild_id: String,
19 members: Vec<MemberInfo>,
20}
21
22pub async fn start_http_server(
23 ctx: Arc<Context>,
24 data: Arc<Data>,
25 port: u16,
26) -> color_eyre::eyre::Result<()> {
27 let addr = format!("127.0.0.1:{port}");
28 let listener = TcpListener::bind(&addr).await?;
29
30 println!("HTTP server listening on http://{addr}");
31
32 loop {
33 let (stream, _) = listener.accept().await?;
34 let ctx = Arc::clone(&ctx);
35 let data = Arc::clone(&data);
36
37 tokio::spawn(async move {
38 if let Err(e) = handle_connection(stream, ctx, data).await {
39 eprintln!("Error handling connection: {e}");
40 }
41 });
42 }
43}
44
45async fn handle_connection(
46 mut stream: TcpStream,
47 ctx: Arc<Context>,
48 _data: Arc<Data>,
49) -> color_eyre::eyre::Result<()> {
50 let mut buffer = [0; 1024];
51 stream.read(&mut buffer).await?;
52
53 let request = String::from_utf8_lossy(&buffer[..]);
54 let request_line = request.lines().next().unwrap_or("");
55
56 if request_line.starts_with("GET /members") {
57 let guild_id = extract_guild_id_from_request(request_line)
58 .unwrap_or(GuildId::new(1095080242219073606));
59
60 match get_guild_members(&ctx, guild_id).await {
61 Ok(response_body) => {
62 let response = format!(
63 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
64 response_body.len(),
65 response_body
66 );
67 stream.write_all(response.as_bytes()).await?;
68 }
69 Err(e) => {
70 let error_body = format!(r#"{{"error": "Failed to fetch members: {e}"}}"#);
71 let response = format!(
72 "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
73 error_body.len(),
74 error_body
75 );
76 stream.write_all(response.as_bytes()).await?;
77 }
78 }
79 } else {
80 let not_found = r#"{"error": "Not found. Use /members?guild_id=YOUR_GUILD_ID"}"#;
81 let response = format!(
82 "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
83 not_found.len(),
84 not_found
85 );
86 stream.write_all(response.as_bytes()).await?;
87 }
88
89 Ok(())
90}
91
92fn extract_guild_id_from_request(request_line: &str) -> Option<GuildId> {
93 if let Some(query_start) = request_line.find('?') {
94 let query = &request_line[query_start + 1..];
95 let params: HashMap<&str, &str> = query
96 .split('&')
97 .filter_map(|param| {
98 let mut parts = param.split('=');
99 Some((parts.next()?, parts.next()?))
100 })
101 .collect();
102
103 if let Some(guild_id_str) = params.get("guild_id") {
104 if let Ok(guild_id) = guild_id_str.parse::<u64>() {
105 return Some(GuildId::new(guild_id));
106 }
107 }
108 }
109 None
110}
111
112async fn get_guild_members(ctx: &Context, guild_id: GuildId) -> color_eyre::eyre::Result<String> {
113 let members = guild_id.members(&ctx.http, None, None).await?;
114
115 let member_infos: Vec<MemberInfo> = members
116 .iter()
117 .map(|member| MemberInfo {
118 id: member.user.id.to_string(),
119 username: member.user.name.clone(),
120 avatar_url: member.user.avatar_url(),
121 })
122 .collect();
123
124 let response = MembersResponse {
125 guild_id: guild_id.to_string(),
126 members: member_infos,
127 };
128
129 Ok(serde_json::to_string_pretty(&response)?)
130}