moderatior
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9300bea --- /dev/null +++ b/src/main.rs @@ -0,0 +1,235 @@ +#![feature(if_let_guard, let_chains)] +use anyhow::Result; +use emoji::named::*; +use poise::{serenity_prelude::*, CreateReply}; +use std::convert::identity; +use std::fs::read_to_string; +use std::pin::pin; +use std::sync::Arc; +use std::time::Duration; + +#[macro_export] +macro_rules! send { + ($e:expr, $fmt:literal $(, $args:expr)* $(,)?) => { + $e.send(format!($fmt $(, $args)*)) + }; +} + +fn last() -> u64 { + std::fs::read("last") + .ok() + .map(|x| { + x.into_iter() + .fold(0u64, |acc, x| acc * 10 + (x - b'0') as u64) + }) + .unwrap_or(0) +} + +fn set_last(x: u64) { + std::fs::write("last", x.to_string()).unwrap(); +} + +pub fn format(log: AuditLogEntry) -> Option<String> { + use serenity::model::guild::audit_log::Action::*; + use serenity::model::guild::audit_log::Change::*; + let changes = log + .changes + .iter() + .flatten() + .into_iter() + .map(|x| { + Some(match x { + Nick { old, new } => { + format!("nick ~~{}~~ {RIGHT} {}", old.as_ref()?, new.as_ref()?) + } + Name { old, new } => { + format!("name ~~{}~~ {RIGHT} {}", old.as_ref()?, new.as_ref()?) + } + RolesRemove { new, .. } => new + .as_ref()? + .into_iter() + .map(|x| format!("{ADD} <@&{}>", x.id)) + .reduce(|a, b| format!("{a} {b}")) + .unwrap_or_else(String::new), + RolesAdded { new, .. } => new + .as_ref()? + .into_iter() + .map(|x| format!("{CANCEL} <@&{}>", x.id)) + .reduce(|a, b| format!("{a} {b}")) + .unwrap_or_else(String::new), + _ => return None, + }) + }) + .filter_map(identity) + .reduce(|a, b| format!("{a}\n{b}")) + .unwrap_or_else(String::new); + Some(format!( + "<t:{}:d> <@{}> {}", + log.id.created_at().unix_timestamp(), + log.user_id, + match log.action { + GuildUpdate => format!("guild changes\n{changes}"), + Channel(ChannelAction::Create) => format!("{ADD} channel\n{changes}"), + Channel(ChannelAction::Delete) => format!("{CANCEL} channel\n{changes}"), + Channel(ChannelAction::Update) => format!("{ROTATE} channel\n{changes}"), + Member(MemberAction::Kick | MemberAction::BanAdd) => + format!("{HAMMER} <@{}>", log.target_id?), + Member(MemberAction::RoleUpdate) + if let Some(t) = log.target_id + && t.get() != log.user_id.get() => + { + format!("{ROTATE} roles of <@{t}>: {changes}") + } + Member(MemberAction::RoleUpdate) => format!("{ROTATE} roles: {changes}"), + Message(MessageAction::Delete) => + format!("{CANCEL} deleted message by <@{}>", log.target_id?), + _ => return None, + } + )) +} + +const PFX: char = '}'; + +async fn lop(c: serenity::client::Context) { + let c = &c; + let g = c.http().get_guild(925674713429184564.into()).await.unwrap(); + let ch = g.channel_id_from_name(c, "server-logs").unwrap(); + loop { + let l = last(); + for log in g + .audit_logs(c, None, None, None, None) + .await + .unwrap() + .entries + .into_iter() + .rev() + .filter(|x| x.id.created_at().unix_timestamp() as u64 > l) + { + let Some(h) = format(log.clone()) else { + continue; + }; + ch.send_message( + c, + CreateMessage::new() + .allowed_mentions(CreateAllowedMentions::new().empty_users().empty_roles()) + .content(h), + ) + .await + .unwrap(); + } + set_last( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + // every minute + tokio::time::sleep(Duration::from_secs(60)).await; + } +} +pub struct Bot; +impl Bot { + pub async fn spawn() { + println!("bot startup"); + let tok = + std::env::var("TOKEN").unwrap_or_else(|_| read_to_string("token").expect("wher token")); + let f = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: vec![help(), prune(), run()], + on_error: |e| Box::pin(on_error(e)), + prefix_options: poise::PrefixFrameworkOptions { + edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( + std::time::Duration::from_secs(2 * 60), + ))), + prefix: Some(PFX.to_string()), + ..Default::default() + }, + ..Default::default() + }) + .setup(|ctx, _ready, f| { + Box::pin(async move { + poise::builtins::register_globally(ctx, &f.options().commands).await?; + println!("registered"); + Ok(()) + }) + }) + .build(); + ClientBuilder::new( + tok, + GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT, + ) + .framework(f) + .await + .unwrap() + .start() + .await + .unwrap(); + } +} +type Context<'a> = poise::Context<'a, (), anyhow::Error>; + +async fn on_error(error: poise::FrameworkError<'_, (), anyhow::Error>) { + use poise::FrameworkError::Command; + match error { + Command { error, ctx, .. } => { + ctx.say(format!("<@696196765564534825> {error}")) + .await + .unwrap(); + } + err => poise::builtins::on_error(err).await.unwrap(), + } +} + +#[poise::command(slash_command)] +/// ask for information +pub async fn help(ctx: Context<'_>) -> Result<()> { + ctx.send( + poise::CreateReply::default() + .ephemeral(true) + .content(include_str!("help.md")), + ) + .await?; + Ok(()) +} +const OWNER: u64 = 696196765564534825; +#[poise::command(slash_command)] +/// Run the logger +pub async fn run(c: Context<'_>) -> Result<()> { + if c.author().id != OWNER { + poise::say_reply(c, "access denied. this incident will be reported").await?; + return Ok(()); + } + poise::say_reply(c, OK).await?; + tokio::spawn(lop(c.serenity_context().clone())); + Ok(()) +} + +#[poise::command(slash_command)] +/// Prune own messages. +pub async fn prune(c: Context<'_>) -> Result<()> { + if c.author().id != OWNER { + poise::say_reply(c, "access denied. this incident will be reported").await?; + return Ok(()); + } + let h = poise::say_reply(c, "working...").await?; + let hm = h.message().await.unwrap().id; + let g = c.http().get_guild(925674713429184564.into()).await.unwrap(); + let ch = g.channel_id_from_name(c, "server-logs").unwrap(); + let mut strm = pin!(ch.messages_iter(c)); + while let Some(Ok(next)) = futures::StreamExt::next(&mut strm).await { + if next.id == hm { + continue; + } + if next.is_own(c) { + next.delete(c).await?; + } + } + h.edit(c, CreateReply::default().content(OK)).await?; + std::fs::write("last", &[b'0']).unwrap(); + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + Bot::spawn().await; +} |