generated from Oliver/discord-bot-template
220 lines
7.6 KiB
Rust
220 lines
7.6 KiB
Rust
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<Channel>,
|
|
orphanage: Vec<GuildChannel>,
|
|
needs_clean: bool,
|
|
}
|
|
|
|
struct Channel {
|
|
this: GuildChannel,
|
|
children: Vec<Channel>,
|
|
messages: Vec<Message>,
|
|
}
|
|
|
|
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<Channel>) {
|
|
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<Channel>, 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<Cache>, &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<Channel>, 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<Channel>) -> 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<ChannelId, GuildChannel>) -> 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(())
|
|
}
|