use std::{collections::HashMap, fmt::Display, sync::Arc}; use crate::Context; use anyhow::Error; use poise::serenity_prelude::{Cache, CacheHttp, ChannelId, ChannelType, GetMessages, GuildChannel, Http, Message}; use tracing::error; struct Server { channels: Vec, orphanage: Vec, needs_clean: bool, } struct Channel { this: GuildChannel, children: Vec, messages: Vec, } impl Channel { fn new(this: GuildChannel) -> Self { Self { this, // Empty vecs don't allocate until a push children: Vec::new(), messages: Vec::new(), } } } impl Display for Server { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn print(f: &mut std::fmt::Formatter<'_>, tab: usize, channel: &Vec) { for i in channel { for _ in 0..tab { let _ = write!(f, "\t"); } let _ = writeln!(f, "{} {}", prefix(i.this.kind),i.this.name); print(f, tab+1, &i.children); } } fn prefix(kind: ChannelType) -> &'static str { match kind { poise::serenity_prelude::ChannelType::Text => "๐Ÿ’ฌ", poise::serenity_prelude::ChannelType::Private => "๐Ÿ•ต๏ธ", poise::serenity_prelude::ChannelType::Voice => "๐Ÿ”Š", poise::serenity_prelude::ChannelType::GroupDm => "๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", poise::serenity_prelude::ChannelType::Category => "๐Ÿ“", poise::serenity_prelude::ChannelType::News => "๐Ÿ“ข", poise::serenity_prelude::ChannelType::NewsThread => "๐Ÿ“ข๐Ÿงต", poise::serenity_prelude::ChannelType::PublicThread => "๐Ÿงต", poise::serenity_prelude::ChannelType::PrivateThread => "๐Ÿ•ต๏ธ๐Ÿงต", poise::serenity_prelude::ChannelType::Stage => "๐ŸŽ™๏ธ", poise::serenity_prelude::ChannelType::Directory => "๐Ÿ“’", poise::serenity_prelude::ChannelType::Forum => "๐Ÿ—ฃ๏ธ", _ => "?", } } print(f, 0, &self.channels); if self.needs_clean { let _ = writeln!(f, "Orphans: (please clean() before displaying...)"); for i in &self.orphanage { let _ = write!(f, "{} {},", prefix(i.kind),i.name); } } write!(f, "") } } impl Server { fn new() -> Self { Self { channels: Vec::new(), orphanage: Vec::new(), needs_clean: false } } // TODO this might be broken fn search_by_id<'a>(target: &'a mut Vec, find: &ChannelId) -> Option<&'a mut Channel> { for child in target { if child.this.id == *find { return Some(child); } match Self::search_by_id(&mut child.children, find) { Some(x) => return Some(x), None => {}, } } None } fn add(&mut self, insert: GuildChannel) { // make sure the new item wants a parent if let Some(parent_id) = &insert.parent_id { // find the parent (needs to go thru all nodes) match Self::search_by_id(&mut self.channels, &parent_id) { Some(parent_node) => { parent_node.children.push(Channel::new(insert)); }, None => { // couldn't find parent, store somewhere else until it's parent is added... self.orphanage.push(insert); self.needs_clean = true; }, } } else { self.channels.push(Channel::new(insert)); } } /// Cleans out the orphan channels, finding them parents. You'll want to use this before displaying anything. fn clean(&mut self) { if !self.needs_clean {return;} // Look thru the orphanage and try to find parents for orphan in &self.orphanage { if let Some(parent_id) = orphan.parent_id { if let Some(found) = Self::search_by_id(&mut self.channels, &parent_id) { found.children.push(Channel::new(orphan.clone())); } else { panic!("โš ๏ธ Couldn't find parent for orphan!"); } } else { panic!("โš ๏ธ All orphans should want a parent node!"); } } self.orphanage.clear(); self.needs_clean = false; } /// Scrapes messages for all the channels in `self`. async fn scrape_all(&mut self) { let settings = GetMessages::default().limit(5); let cache: (&Arc, &Http) = (&Arc::new(Cache::new()), &Http::new(&crate::ENV.token)); walk_channels(&mut self.channels, cache, settings).await; // recursive walk thru the channels async fn walk_channels(all: &mut Vec, cache: impl CacheHttp + Clone, settings: GetMessages) { for channel in all { // get the messages match Box::pin(channel.this.messages(cache.clone(), settings)).await { Ok(mesgs) => { // store messages in our server object channel.messages = mesgs; if channel.messages.is_empty() { error!("{} was empty - (Or incorrect permissions)", channel.this.name); } }, Err(e) => { error!("{}", e); }, } // Clone *should* be cheap - it's Arc under the hood Box::pin(walk_channels(&mut channel.children, cache.clone(), settings)).await; } } } /// Walk thru all the channels and count the saved messages. Will only give relevant data if /// done after `scrape_all()`. fn message_count(&self) -> usize { fn walk(this: &Vec) -> usize { let mut total = 0; for channel in this { total += walk(&channel.children); }; total } walk(&self.channels) } } #[poise::command(slash_command, rename = "scrape_all", guild_only)] pub async fn scrape_all(ctx: Context<'_>) -> Result<(), Error> { let guild = ctx.guild_id().unwrap().to_partial_guild(ctx.serenity_context()).await.unwrap(); if let Ok(map) = guild.channels(ctx.http()).await { let mut server = index(map).await; server.scrape_all().await; let _ = ctx.reply(&format!("Scraped {} messages", server.message_count())).await; } Ok(()) } /// Get server's topology (and runs clean) async fn index(map: HashMap) -> Server { let mut server = Server::new(); // iterate thru all channels map.into_iter().for_each(|(_id, current)| { // println!("{} {} {:?}", current.name, current.id, current.parent_id); server.add(current); // TODO Take note of position // TODO Take node of vc user limit }); server.clean(); server } // NOTE!!! Make sure these names in quotes are lowercase! #[poise::command(slash_command, rename = "index", guild_only)] pub async fn index_cmd(ctx: Context<'_>) -> Result<(), Error> { let guild = ctx.guild_id().unwrap().to_partial_guild(ctx.serenity_context()).await.unwrap(); match guild.channels(ctx.http()).await { Ok(ok) => { let server = index(ok).await; let _ = ctx.reply(server.to_string()).await; }, Err(_) => todo!(), } Ok(()) }