moderatior
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs235
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;
+}