moderatior
delete events
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/db.rs | 41 | ||||
| -rw-r--r-- | src/main.rs | 129 |
4 files changed, 161 insertions, 15 deletions
@@ -1,3 +1,4 @@ Cargo.lock token -last
\ No newline at end of file +last +db1
\ No newline at end of file @@ -22,6 +22,9 @@ anyhow = "1.0.75" jemallocator-global = "0.3.2" ahash = "0.8.11" emoji = { git = "https://github.com/apricot-conservation-project/emoji", version = "0.1.0" } +kv = { version = "0.24.0", features = ["bincode-value"] } +sled = { version = "0.34.7", features = ["compression"] } +diff = "0.1.13" [profile.release] strip = true diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..448ea6a --- /dev/null +++ b/src/db.rs @@ -0,0 +1,41 @@ +// TODO pruning? +use std::sync::LazyLock; + +use kv::*; + +fn cfg() -> kv::Config { + kv::Config { + path: "./db1".into(), + temporary: false, + use_compression: true, + flush_every_ms: None, + cache_capacity: None, + segment_size: None, + } +} +static DB: LazyLock<Store> = LazyLock::new(|| Store::new(cfg()).unwrap()); +static BU: LazyLock<Bucket<Integer, Bincode<(String, Vec<String>, u64)>>> = + LazyLock::new(|| DB.bucket(None).unwrap()); + +pub fn set(k: u64, v: (String, Vec<String>, u64)) { + BU.set(&k.into(), &Bincode(v)).unwrap(); +} +pub fn get(k: u64) -> Option<(String, Vec<String>, u64)> { + BU.get(&k.into()).unwrap().map(|x| x.0) +} +pub fn sz() -> f32 { + DB.size_on_disk().unwrap() as f32 / (1 << 20) as f32 +} +pub fn set_m(m: poise::serenity_prelude::Message) { + set( + m.id.get(), + ( + m.content, + m.attachments + .into_iter() + .map(|x| x.url) + .collect::<Vec<String>>(), + m.author.id.into(), + ), + ) +} diff --git a/src/main.rs b/src/main.rs index b95bd8d..652775d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,12 @@ -#![feature(if_let_guard, let_chains)] +#![feature(if_let_guard, let_chains, lazy_cell)] 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; - +mod db; #[macro_export] macro_rules! send { ($e:expr, $fmt:literal $(, $args:expr)* $(,)?) => { @@ -81,15 +80,11 @@ pub fn format(log: AuditLogEntry) -> Option<String> { 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(); @@ -135,14 +130,87 @@ impl Bot { 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()], + commands: vec![help(), prune(), reload(), 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() + event_handler: |c, e, _, _| { + Box::pin(async move { + match e { + FullEvent::Message { new_message } => db::set_m(new_message.clone()), + FullEvent::MessageUpdate { event: MessageUpdateEvent { id, channel_id, content: Some(content), author: Some(User{ id: author, bot, ..}), attachments: Some(attachments), .. }, .. } if !bot => { + if let Some((oc, _, _)) = db::get(id.get()) { + let diff = diff::lines(content, &oc).into_iter().map(|diff| { + match diff { + diff::Result::Left(l) => format!("+ {l}"), + diff::Result::Right(r) => format!("- {r}"), + diff::Result::Both(l, _) => format!(" {l}"), + } + }).fold(String::new(), |a,b| format!("{a}\n{b}")); + ChannelId::new(1226396559185285280) + .send_message( + c, + CreateMessage::new() + .allowed_mentions( + CreateAllowedMentions::new() + .empty_users() + .empty_roles(), + ) + .content(format!("<t:{}:d> <@{author}> edited their message https://discord.com/channels/925674713429184564/{channel_id}/{id}\n```diff{diff}```", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs())), + ) + .await + .unwrap(); + } + db::set(id.get(), (content.clone(), attachments.iter().map(|a|a.url.clone()).collect(), author.get())) + }, + FullEvent::MessageDelete { + deleted_message_id, .. + } => { + let log = c.http().get_guild(925674713429184564.into()).await.unwrap() + .audit_logs(c, Some(audit_log::Action::Message(MessageAction::Delete)), None, None, Some(1)).await? + .entries.into_iter().next().unwrap(); + let (author, who) = (log.target_id.unwrap(), log.user_id); + ChannelId::new(1226396559185285280) + .send_message( + c, + CreateMessage::new() + .allowed_mentions( + CreateAllowedMentions::new() + .empty_users() + .empty_roles(), + ) + .content(match db::get(deleted_message_id.get()) { + Some((content, links, a)) => { + if a == 1224510735959462068 { return Ok(()) } + if author.get() != a { + format!( + "<t:{}:d> <@{a}> {CANCEL} deleted their own message:\n{content}\n\n{}", + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), + links + .into_iter() + .reduce(|a, b| format!("{a} {b}")) + .unwrap_or_else(String::new) + ) + } else { + format!( + "<t:{}:d> <@{who}> {CANCEL} deleted message by <@{author}>:\n{content}\n\n{}", + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), + links + .into_iter() + .reduce(|a, b| format!("{a} {b}")) + .unwrap_or_else(String::new) + ) + } + } + None => format!("<@{who}> {CANCEL} deleted message by <@{author}>"), + + }), + ) + .await + .unwrap(); + } + _ => (), + } + Ok(()) + }) }, ..Default::default() }) @@ -233,3 +301,36 @@ pub async fn prune(c: Context<'_>) -> Result<()> { async fn main() { Bot::spawn().await; } + +#[poise::command(slash_command)] +pub async fn reload(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, "0 complete").await?; + let mut n = 0u64; + let chs = c.guild().unwrap().channels.clone(); + for ch in chs.keys() { + let mut stream = pin!(ch.messages_iter(c)); + while let Some(Ok(next)) = futures::StreamExt::next(&mut stream).await { + if db::get(next.id.get()).is_some() { + break; + } + n += 1; + db::set_m(next); + } + _ = h + .edit( + c, + CreateReply::default().content(format!("+{n}, {:.2} mbs", db::sz())), + ) + .await; + } + h.edit( + c, + CreateReply::default().content(format!("+{n}: {:.2} mbs", db::sz())), + ) + .await?; + Ok(()) +} |