mod logic;
mod map;
pub mod ownership;
pub mod repos;
mod schematic;
pub mod search;
mod sorter;
mod db;
mod data;
pub use data::log;
use crate::emoji;
use anyhow::Result;
use dashmap::DashMap;
use mindus::data::DataWrite;
use mindus::Serializable;
use poise::{serenity_prelude::*, CreateReply};
use repos::{Repo, FORUMS, SPECIAL, THREADED};
use serenity::futures::StreamExt;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::fs::read_to_string;
use std::ops::ControlFlow;
use std::path::Path;
use std::process::Stdio;
use std::sync::{Arc, LazyLock, OnceLock};
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
pub fn clone() {
for repos::Repo { auth, name, .. } in repos::ALL {
if !Path::new(&format!("repos/{name}")).exists() {
assert_eq!(
std::process::Command::new("git")
.current_dir("repos")
.arg("clone")
.args(["--depth", "5"])
.arg(auth)
.arg(format!("{name}"))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.unwrap()
.code()
.unwrap(),
0
);
}
}
}
#[derive(Debug)]
pub struct Data {
// message -> resp
tracker: Arc<DashMap<MessageId, Message>>,
}
pub struct Msg {
avatar: String,
author: String,
locale: String,
author_id: u64,
guild: u64,
content: String,
channel: ChannelId,
attachments: Vec<Attachment>,
}
#[macro_export]
macro_rules! send {
($e:expr, $fmt:literal $(, $args:expr)* $(,)?) => {
$e.send(format!($fmt $(, $args)*))
};
}
const SUCCESS: (u8, u8, u8) = (34, 139, 34);
const PFX: char = '}';
fn tags<T: std::fmt::Display>(t: &[T]) -> String {
if let [x, rest @ ..] = t {
let mut s = format!("[\"{x}\"");
for elem in rest {
write!(s, ",\"{elem}\"").unwrap();
}
write!(s, "]").unwrap();
s
} else {
String::from("[]")
}
}
#[derive(Clone, Debug)]
pub struct Ch {
repo: &'static Repo,
d: &'static str,
ty: Type,
}
#[derive(Clone, Debug)]
enum Type {
Basic(&'static [&'static str]),
Assembled(String),
Forum(()), // &'static phf::Map<&'static str, &'static [&'static str]>
}
fn sep(x: Option<&Ch>) -> (Option<&'static str>, Option<String>, Option<&Repo>) {
(
x.map(|x| x.d),
x.and_then(|x| match x.ty.clone() {
Type::Basic(x) => Some(tags(x)),
Type::Forum(_) => None,
Type::Assembled(x) => Some(x),
}),
x.map(|x| x.repo),
)
}
const OWNER: u64 = 696196765564534825;
#[poise::command(slash_command)]
/// This command reads all messages to find the schems.
/// This command will possibly add denied schems.
pub async fn scour(
c: Context<'_>,
#[description = "the channel in question"] ch: ChannelId,
) -> Result<()> {
let repo = repos::chief!(c);
let mut n = 0;
let d = SPECIAL[&ch.get()].d;
let h = c.say(format!("scouring {d}...")).await?;
_ = std::fs::create_dir(format!("repos/{}/{d}", repo.name));
let Ch { d, repo, ty, .. } = SPECIAL.get(&ch.get()).unwrap();
match ty {
Type::Basic(tg) => {
let mut msgs = ch.messages_iter(c).boxed();
let tags = tags(tg);
while let Some(msg) = msgs.next().await {
let Ok(msg) = msg else {
continue;
};
if let Ok(Some(mut x)) = schematic::from((&msg.content, &msg.attachments)).await {
x.schem.tags.insert("labels".into(), tags.clone());
let who = msg.author_nick(c).await.unwrap_or(msg.author.name.clone());
ownership::get(repo)
.await
.insert(msg.id.get(), (msg.author.name.clone(), msg.author.id.get()));
repo.write(d, msg.id, x);
repo.commit(&who, &format!("add {:x}.msch", msg.id.get()));
msg.react(c, emojis::get!(MERGE)).await?;
n += 1;
}
}
}
_ => {
unreachable!()
}
}
repo.push();
h.edit(
c,
poise::CreateReply::default().content(format!(
"done scouring <#{ch}>! <:merge:1192387272046284800> {n} schems"
)),
)
.await?;
Ok(())
}
async fn del(c: &serenity::prelude::Context,& Ch{ d:dir, repo: git, ..}: &Ch, deleted_message_id: u64) {
use crate::emoji::named::*;
if let Ok(s) = git.schem(dir, deleted_message_id.into()){
let own = git.own().await.erase(deleted_message_id).unwrap();
git.remove(dir, deleted_message_id.into());
git.commit("plent", &format!("remove {deleted_message_id:x}"));
git.push();
if git == &repos::DESIGN_IT && !cfg!(debug_assertions) {
send(c,|x| x
.username("plent")
.embed(CreateEmbed::new().color(RM)
.description(format!("{CANCEL} remove {} (added by {own}) (`{:x}.msch`)", emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), deleted_message_id))
.footer(CreateEmbedFooter::new("message was deleted.")
))
).await;
};
}
}
static HOOK: OnceLock<Webhook> = OnceLock::new();
pub async fn hookup(c: &impl AsRef<Http>) {
let v = Webhook::from_url(c, {
&std::env::var("WEBHOOK")
.unwrap_or_else(|_| read_to_string("webhook").expect("wher webhook"))
})
.await
.unwrap();
HOOK.get_or_init(|| v);
}
async fn send<F>(c: &impl AsRef<Http>, block: F)
where
for<'b> F: FnOnce(ExecuteWebhook) -> ExecuteWebhook,
{
let execute_webhook = ExecuteWebhook::default();
let execute_webhook = block(execute_webhook);
if let Err(e) = HOOK
.get()
.unwrap()
.execute(c.as_ref(), false, execute_webhook.clone())
.await
{
println!("sending {execute_webhook:#?} got error {e}.");
}
}
const RM: (u8, u8, u8) = (242, 121, 131);
const AD: (u8, u8, u8) = (128, 191, 255);
const CAT: &str =
"https://cdn.discordapp.com/avatars/696196765564534825/6f3c605329ffb5cfb790343f59ed355d.webp";
async fn handle_message(
c: &poise::serenity_prelude::Context,
new_message: &Message,
d: &Data,
) -> Result<()> {
let who = new_message
.author_nick(c)
.await
.unwrap_or(new_message.author.name.clone());
let post = EXTRA.get(&new_message.channel_id.get()).map(|x| x.clone());
let (dir, l, repo) = sep(SPECIAL.get(&new_message.channel_id.get()).or(post.as_ref()));
let m = Msg {
author: who.clone(),
locale: new_message.author.locale.clone().unwrap_or("unknown locale".to_string()),
author_id: new_message.author.id.get(),
guild: new_message.guild_id.map_or(0,Into::into),
avatar: new_message.author.avatar_url().unwrap_or(CAT.to_string()),
attachments: new_message.attachments.clone(),
content: new_message.content.clone(),
channel: new_message.channel_id,
};
let x = schematic::with(m, c, l).await?;
match x {
ControlFlow::Continue(())
if THREADED.contains(&new_message.channel_id.get())
|| SPECIAL.contains_key(&new_message.channel_id.get()) =>
{
new_message.delete(c).await?;
return Ok(());
}
ControlFlow::Break((m, n, s)) => {
if SPECIAL.contains_key(&m.channel_id.get()) || THREADED.contains(&m.channel_id.get()) {
m.channel_id
.create_thread_from_message(
c,
m.id,
CreateThread::new(n)
.audit_log_reason("because yes")
.auto_archive_duration(AutoArchiveDuration::OneDay),
)
.await
.unwrap();
}
if let Some(dir) = dir
&& let Some(repo) = repo
{
println!("adding {dir}");
// add :)
repo.own().await.insert(
new_message.id.get(),
(new_message.author.name.clone(), new_message.author.id.get()),
);
use emoji::named::*;
if repo.name == "DESIGN_IT" && !cfg!(debug_assertions) {
send(c,|x| x
.avatar_url(new_message.author.avatar_url().unwrap_or(CAT.to_string()))
.username(&who)
.embed(CreateEmbed::new().color(AD)
.description(format!("https://discord.com/channels/925674713429184564/{}/{} {ADD} add {} (`{:x}.msch`)", m.channel_id,m.id, emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), new_message.id.get())))
).await;
}
if post.is_some() {
EXTRA.remove(&new_message.channel_id.get());
db::set(new_message.channel_id.get(), new_message.id.get());
}
repo.write(dir, new_message.id, s);
repo.add();
repo.commit(&who, &format!("add {:x}.msch", new_message.id.get()));
repo.push();
new_message.react(c, emojis::get!(MERGE)).await?;
}
d.tracker.insert(new_message.id, m);
return Ok(());
}
_ => (),
};
// not tracked, as you cant add a attachment afterwwards.
map::with(new_message, c).await?;
Ok(())
}
static SEEN: LazyLock<Mutex<HashSet<(GuildId, u64, String, UserId)>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
static EXTRA: LazyLock<DashMap<u64, Ch>> = LazyLock::new(DashMap::new);
pub struct Bot;
impl Bot {
pub async fn spawn() {
use emoji::named::*;
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![logic::run(), lb(), logic::run_file(), sorter::sorter(), sorter::mapper(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), rename(), rename_file(), render(), render_file(), render_message(), map::render_message()],
event_handler: |c, e, _, d| {
Box::pin(async move {
match e {
FullEvent::Ready { .. } => {
println!("bot ready");
while SEEN.lock().await.len() < 5 {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
let mut x = SEEN.lock().await.clone().into_iter().collect::<Vec<_>>();
x.sort_by_key(|(_, x, _,_)|*x);
for (g, _, _ ,_ ) in x.iter().take_while(|(_, x, _,_)| *x <= 10).filter(|(_, _, _,x)| *x != OWNER) {
// println!()
// g.leave(&c).await.unwrap();
};
for (i, member_count, name, _) in x {
println!(
"{name} has {member_count:?} members {i:?}"
);
}
SEEN.lock().await.clear();
emojis::load(c.http()).await;
hookup(c.http()).await;
}
FullEvent::GuildCreate { guild , ..} => {
SEEN.lock().await.insert((guild.id, guild.member_count, guild.name.clone(), guild.owner_id));
// let User{id,name:owner_name,..} = c.http().get_user(*owner_id).await.unwrap();
}
// :deny:, @vd
FullEvent::ReactionAdd { add_reaction: Reaction {
message_id,
emoji: ReactionType::Custom { id, .. },
channel_id,
member: Some(m @ Member {
nick,
user,
.. }), .. }
}
if let Some(Ch {
d: dir,
repo: git,
.. }) = SPECIAL.get(&channel_id.get()).or(
channel_id.to_channel(c.http()).await
.ok().and_then(|x| x.guild())
.and_then(|x| x.parent_id)
.and_then(|x| FORUMS.get(&x.get())),
)
&& *id == git.deny_emoji
&& git.auth(m)
=> {
// repos::ALL.into_iter().filter(|x|x.own().await.get(k))
let m = c.http().get_message(*channel_id,* message_id).await?;
if let Ok(s) = git.schem(dir,*message_id) {
_ = db::remove(channel_id.get());
let who = nick.as_deref().unwrap_or(&user.name);
let own = ownership::get(git).await.erase(*message_id).unwrap();
git.remove(dir, *message_id);
git.commit(who, &format!("remove {:x}.msch", message_id.get()));
git.push();
_ = m.delete_reaction(c,Some(1174262682573082644.into()), emojis::get!(MERGE)).await;
m.delete_reaction(c,Some(1174262682573082644.into()), ReactionType::Custom { animated: false, id: 1192316518395039864.into(), name: Some("merge".into()) }).await.unwrap();
m.react(c,emojis::get!(DENY)).await?;
// only design-it has a webhook (possibly subject to future change)
if git.name == "DESIGN_IT" && !cfg!(debug_assertions) {
send(c,|x| x
.avatar_url(user.avatar_url().unwrap_or(CAT.to_string()))
.username(who)
.embed(CreateEmbed::new().color(RM)
.description(format!("https://discord.com/channels/925674713429184564/{channel_id}/{message_id} {} {} (added by {own}) (`{:x}`)", emojis::get!(DENY), emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), message_id.get())))
).await;
}
};
}
FullEvent::Message { new_message } => {
if new_message.content.starts_with('!')
|| new_message.content.starts_with(PFX)
|| new_message.author.bot
{
return Ok(());
}
handle_message(c, new_message, d).await?;
},
FullEvent::ThreadCreate { thread } if let Some(Ch{repo, d, ty: Type::Forum(_)}) = repos::FORUMS.get(&thread.parent_id.unwrap().get()) => {
let tg = thread.guild(c).unwrap().channels[&thread.parent_id.unwrap()].available_tags.iter()
.filter(|x| {
thread.applied_tags.contains(&x.id)
}).map(|x| x.name.clone()).collect::<Vec<_>>();
EXTRA.insert(thread.id.get(), Ch { repo, d, ty: Type::Assembled(tags(&*tg)) });
}
FullEvent::MessageUpdate {event: MessageUpdateEvent {
author: Some(author),
guild_id: Some(guild_id),
content: Some(content),
attachments: Some(attachments),
id,
channel_id,
..
}, ..} => {
if let Some((_, r)) = d.tracker.remove(id) {
_ = r.delete(c).await;
let who = author
.nick_in(c, guild_id)
.await
.unwrap_or(author.name.clone());
let (dir, l, repo) = sep(SPECIAL.get(&r.channel_id.get()));
if let ControlFlow::Break((m,_,s)) = schematic::with(
Msg {
locale:author.locale.clone().unwrap_or("unknown locale".to_string()),
author_id: author.id.get(),
avatar: author.avatar_url().unwrap_or(CAT.to_string()),
author: who.clone(),
content:content.clone(),
guild: r.guild_id.map_or(0,Into::into),
attachments:attachments.clone(),
channel: *channel_id,
},
c,
l
)
.await?
{
d.tracker.insert(*id, m);
if let Some(dir) = dir && let Some(git) = repo && git.has(dir, *id) {
// update :)
if *guild_id == 925674713429184564 && !cfg!(debug_assertions) {
send(c,|x| x
.avatar_url(author.avatar_url().unwrap_or(CAT.to_string()))
.username(&who)
.embed(CreateEmbed::new().color(AD)
.description(format!("https://discord.com/channels/925674713429184564/{channel_id}/{id} {ROTATE} update {} (`{:x}.msch`)", emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), id.get())))
).await;
}
git.write(dir, *id, s);
git.commit(&who,&format!("update {:x}.msch", id.get()));
git.push();
}
}
}
}
FullEvent::ThreadDelete { thread, .. } if let Some(ch) = FORUMS.get(&thread.parent_id.get()) && let Some(deleted_message_id) = db::remove(thread.id.get()) => del(&c, ch, deleted_message_id).await,
FullEvent::MessageDelete {
deleted_message_id, channel_id, ..
} => {
if let Some(ch) = SPECIAL.get(&channel_id.get()).or(
channel_id.to_channel(c.http()).await
.ok().and_then(|x| x.guild())
.and_then(|x| x.parent_id)
.and_then(|x| FORUMS.get(&x.get()))
) {
_ = db::remove(channel_id.get());
del(&c, ch, deleted_message_id.get()).await;
}
if let Some((_, r)) = d.tracker.remove(deleted_message_id) {
r.delete(c).await.unwrap();
}
}
_ => {}
};
Ok(())
})
},
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, _| {
Box::pin(async move {
poise::builtins::register_globally(
ctx,
&[
logic::run(),
help(),
ping(),
render(),
schembrowser_instructions(),
render_file(),
render_message(),
rename(),
rename_file(),
map::render_message(),
logic::run_file(),
sorter::sorter(),
sorter::mapper(),
],
)
.await?;
poise::builtins::register_in_guild(
ctx,
&[search::search(), lb(), lb_no_vds(), search::file()],
925674713429184564.into(),
)
.await?;
println!("registered");
let tracker = Arc::new(DashMap::new());
let tc = Arc::clone(&tracker);
tokio::spawn(async move {
loop {
// every 10 minutes
tokio::time::sleep(Duration::from_secs(60 * 10)).await;
tc.retain(|_, v: &mut Message| {
// prune messagees older than 3 hours
Timestamp::now().unix_timestamp() - v.timestamp.unix_timestamp()
< 60 * 60 * 3
});
}
});
Ok(Data { tracker })
})
})
.build();
ClientBuilder::new(
tok,
GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT,
)
.framework(f)
.await
.unwrap()
.start()
.await
.unwrap();
}
}
// pub async fn missing(r: &'static Repo) -> impl Iterator<Item = (MessageId, ChannelId)> {
// let lock = r.own().await;
// search::files()
// .map(move |(x, ch)| {
// let f = search::flake(x.file_name().unwrap().to_str().unwrap());
// (lock.map.contains_key(&f), f, ch)
// })
// .filter_map(|(x, m, c)| (!x).then(|| (m.into(), c.into())))
// }
// #[poise::command(slash_command)]
// pub async fn bust_ghosts(c: Context<'_>) -> Result<()> {
// if c.author().id != OWNER {
// poise::say_reply(c, "access denied. this incident will be reported").await?;
// return Ok(());
// }
// let h = c.reply(emoji::named::LOCK_OPEN).await?;
// for (m, ch) in missing().await.collect::<Vec<_>>() {
// let ch = c.guild().unwrap().channels[&ch].clone();
// let User { id, name, .. } = match ch.message(c, m).await {
// Ok(x) => x.author,
// Err(_) => {
// // removes ghosts
// std::fs::remove_file(git::path(&SPECIAL[&ch.id.get()].d, m)).unwrap();
// continue;
// }
// };
// ownership::insert(m.into(), (name, id.get())).await;
// }
// h.edit(c, poise::CreateReply::default().content(emoji::named::LOCK))
// .await?;
// Ok(())
// }
// #[poise::command(slash_command)]
// pub async fn retag(c: Context<'_>, channel: ChannelId) -> Result<()> {
// if c.author().id != OWNER {
// poise::say_reply(c, "access denied. this incident will be reported").await?;
// return Ok(());
// }
// c.defer().await?;
// let tags = tags(SPECIAL[&channel.get()].labels);
// for schem in search::dir(channel.get()).unwrap() {
// let mut s = search::load(&schem);
// let mut v = DataWrite::default();
// s.tags.insert("labels".into(), tags.clone());
// s.serialize(&mut v)?;
// std::fs::write(schem, v.consume())?;
// }
// c.reply(emoji::named::OK).await?;
// Ok(())
// }
// dbg!(m
// .iter()
// .filter(|x| x.roles.contains(&925676016708489227.into()))
// .map(|x| x.user.id.get())
// .collect::<Vec<_>>());
const VDS: &[u64] = &[
1222024015015706668,
742034952077705317,
126381304857100288,
175218107084832768,
221780012372721664,
231505175246798851,
291255752729821185,
301919226078298114,
315827169395998720,
324736330418487317,
325570201837895680,
330298929331699713,
332054403160735765,
343939197738024961,
360488990974935040,
384188568270274581,
387018214103842818,
391302959444656128,
399346439349600256,
404682730190798858,
417607639938236427,
461517080856887297,
464033296012017674,
488243005283631106,
490271325126918154,
514981385660792852,
527626094744961053,
586994631879819266,
595625721129336868,
618507912511225876,
665938033987682350,
696196765564534825,
705503407179431937,
724657758280089701,
729281676441550898,
797211831894016012,
845191508033667096,
];
pub async fn leaderboard(c: Context<'_>, channel: Option<ChannelId>, vds: bool) -> Result<()> {
use emoji::named::*;
c.defer().await?;
let lock = repos::DESIGN_IT.own().await;
let process = |map: HashMap<u64, u16>| {
let mut v = map.into_iter().collect::<Vec<_>>();
v.sort_by_key(|(_, x)| *x);
use std::fmt::Write;
let mut out = String::new();
v.iter()
.rev()
.zip(1..)
.take(5)
.for_each(|((y, z), x)| writeln!(out, "{x}. **<@{y}>**: {z}").unwrap());
out
};
// match channel {
// Some(ch) => {
// let Some(x) = SPECIAL.get(&ch.get()) else {
// poise::say_reply(c, format!("{CANCEL} not a schem channel")).await?;
// return Ok(());
// };
// let mut map = HashMap::new();
// search::dir(ch.get())
// .unwrap()
// .map(|y| lock.map[&search::flake(y.file_name().unwrap().to_str().unwrap())].1)
// .filter(|x| vds || !VDS.contains(x))
// .for_each(|x| *map.entry(x).or_default() += 1);
// poise::say_reply(
// c,
// format!(
// "## Leaderboard of {}\n{}",
// x.ls
// .join("")
// .chars()
// .map(|x| emoji::mindustry::TO_DISCORD[&x])
// .collect::<String>(),
// process(map)
// ),
// )
// }
// None => {
let mut map = std::collections::HashMap::new();
search::files()
.map(|(y, _)| lock.map[&search::flake(y.file_name().unwrap().to_str().unwrap())].1)
.filter(|x| vds || !VDS.contains(x))
.for_each(|x| *map.entry(x).or_default() += 1);
poise::say_reply(c, format!("## Leaderboard\n{}", process(map))).await?;
Ok(())
}
#[poise::command(slash_command)]
/// Show the leaderboard for players with the most contributed schems, optionally in a certain channel.
pub async fn lb(
c: Context<'_>,
#[description = "optional channel filter"] channel: Option<ChannelId>,
) -> Result<()> {
leaderboard(c, channel, true).await
}
#[poise::command(slash_command)]
/// Show the leaderboard for players, excepting verified designers, with the most schemes.
pub async fn lb_no_vds(
c: Context<'_>,
#[description = "optional channel filter"] channel: Option<ChannelId>,
) -> Result<()> {
leaderboard(c, channel, false).await
}
pub mod emojis {
pub const GUILDS: &[u64] = &[1003092764919091282, 925674713429184564];
use poise::serenity_prelude::*;
use std::sync::OnceLock;
macro_rules! create {
($($i: ident),+ $(,)?) => { paste::paste! {
$(pub static $i: OnceLock<Emoji> = OnceLock::new();)+
pub async fn load(c: &Http) {
for &g in GUILDS {
let all = c.get_emojis(g.into()).await.unwrap();
for e in all {
match e.name.as_str() {
$(stringify!([< $i:lower >])=>{let _=$i.get_or_init(||e);},)+
_ => { /*println!("{n} unused");*/ }
}
}
}
$(
$i.get().expect(&format!("{} should be loaded", stringify!($i)));
)+
}
} };
}
create![MERGE, DENY];
macro_rules! get {
($e: ident) => {
crate::bot::emojis::$e.get().unwrap().clone()
};
}
pub(crate) use get;
}
type Context<'a> = poise::Context<'a, Data, anyhow::Error>;
async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) {
use poise::FrameworkError::Command;
match error {
Command { error, ctx, .. } => {
let mut msg;
{
let mut chain = error.chain();
msg = format!("e: `{}`", chain.next().unwrap());
for mut source in chain {
write!(msg, "from: `{source}`").unwrap();
while let Some(next) = source.source() {
write!(msg, "from: `{next}`").unwrap();
source = next;
}
}
}
/* let bt = error.backtrace();
if bt.status() == std::backtrace::BacktraceStatus::Captured {
let parsed = btparse_stable::deserialize(error.backtrace()).unwrap();
let mut s = vec![];
for frame in parsed.frames {
if let Some(line) = frame.line
&& (frame.function.contains("plent")
|| frame.function.contains("poise")
|| frame.function.contains("serenity")
|| frame.function.contains("mindus")
|| frame.function.contains("image"))
{
s.push(format!("l{}@{}", line, frame.function));
}
}
s.truncate(15);
write!(msg, "trace: ```rs\n{}\n```", s.join("\n")).unwrap();
} */
ctx.say(msg).await.unwrap();
}
err => poise::builtins::on_error(err).await.unwrap(),
}
}
pub fn strip_colors(from: &str) -> String {
let mut result = String::new();
result.reserve(from.len());
let mut level: u8 = 0;
for c in from.chars() {
if c == '[' {
level += 1;
} else if c == ']' {
level -= 1;
} else if level == 0 {
result.push(c);
}
}
result
}
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
pub async fn help(
ctx: Context<'_>,
#[description = "command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<()> {
log(&ctx);
macro_rules! pick {
($e:literal, $u:literal) => {
if matches!(
command.as_deref(),
Some("eval") | Some("exec") | Some("run")
) {
include_str!($e)
} else {
include_str!($u)
}
};
}
ctx.send(
poise::CreateReply::default()
.allowed_mentions(CreateAllowedMentions::new())
.content(match ctx.locale() {
Some("ru") => pick!("help_eval_ru.md", "usage_ru.md"),
_ => pick!("help_eval.md", "usage.md"),
}),
)
.await?;
Ok(())
}
pub fn png(p: fimg::Image<Vec<u8>, 3>) -> Vec<u8> {
use oxipng::*;
let p = RawImage::new(
p.width(),
p.height(),
ColorType::RGB {
transparent_color: None,
},
BitDepth::Eight,
p.take_buffer(),
)
.unwrap();
p.create_optimized_png(&oxipng::Options {
filter: indexset! { RowFilter::None },
bit_depth_reduction: false,
color_type_reduction: false,
palette_reduction: false,
grayscale_reduction: false,
..oxipng::Options::from_preset(0)
})
.unwrap()
}
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
/// Pong!
pub async fn ping(c: Context<'_>) -> Result<()> {
log(&c);
use emoji::named::*;
let m = memory_stats::memory_stats().unwrap().physical_mem as f32 / (1 << 20) as f32;
let start = cpu_monitor::CpuInstant::now()?;
std::thread::sleep(Duration::from_millis(200));
let end = cpu_monitor::CpuInstant::now()?;
let duration = end - start;
let util = duration.non_idle() * 100.0;
// let m = (m / 0.1) + 0.5;
// let m = m.floor() * 0.1;
c.reply(format!(
"pong!\n{DISCORD}{RIGHT}: {} — {HOST}: {m:.1}MiB - <:stopwatch:1361892467510870167><:world_processor:1307657404128690268> {util:.0}% — <:up:1307658579251167302><:time:1361892343199957022> {}",
humantime::format_duration(Duration::from_millis(
Timestamp::now()
.signed_duration_since(*c.created_at())
.to_std()?
.as_millis() as _
)),
humantime::format_duration(Duration::from_secs(
Instant::now()
.duration_since(*super::START.get().unwrap())
.as_secs()
))
))
.await?;
Ok(())
}
#[poise::command(
slash_command,
install_context = "User",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
/// Renders base64 schematic.
pub async fn render(c: Context<'_>, #[description = "schematic, base64"] s: String) -> Result<()> {
log(&c);
poise::send_reply(c,
match schematic::from_b64(&s) {
Ok(s) =>
schematic::reply(
s,
&c.author().name,
&c.author().avatar_url().unwrap_or(CAT.to_string()),
)
.await?,
Err(e) =>
CreateReply::default()
.content(format!("schem broken / not schem: {e}")),
})
.await?;
Ok(())
}
#[poise::command(
slash_command,
install_context = "User",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
/// Renders map/msch schematic.
pub async fn render_file(
c: Context<'_>,
#[description = "map / schematic, msch"] s: Attachment,
) -> Result<()> {
log(&c);
_ = c.defer().await;
let Some(s) = schematic::from_attachments(std::slice::from_ref(&s)).await? else {
match map::reply(&s).await? {
ControlFlow::Break(x) => return Ok(drop(poise::send_reply(c, x).await?)),
ControlFlow::Continue(e) if e != "not a map." => {
return Ok(drop(poise::say_reply(c, e).await?))
}
ControlFlow::Continue(_) => (),
};
poise::send_reply(
c,
CreateReply::default()
.content("no schem found")
.ephemeral(true),
)
.await?;
return Ok(());
};
poise::send_reply(
c,
schematic::reply(
s,
&c.author().name,
&c.author().avatar_url().unwrap_or(CAT.to_string()),
)
.await?,
)
.await?;
Ok(())
}
#[poise::command(slash_command)]
/// Rename a schematic.
async fn rename_file(c: Context<'_>, #[description = "schematic, msch"] s: Attachment, #[description = "new name"] name:String) -> Result<()> {
log(&c);
let Some(schematic::Schem{schem: mut s}) = schematic::from_attachments(std::slice::from_ref(&s)).await? else {
c.reply("no schem!").await?;
return Ok(());
};
s.tags.insert("name".to_string(), name);
let mut o= DataWrite::default();
s.serialize(&mut o)?;
poise::send_reply(c, CreateReply::default().attachment(
CreateAttachment::bytes(o.consume(),"out.msch")
)).await?;
Ok(())
}
#[poise::command(slash_command)]
/// Rename a schematic.
async fn rename(c: Context<'_>, #[description = "schematic, base64"] s: String, #[description = "new name"] name:String) -> Result<()> {
log(&c);let Ok(schematic::Schem{schem: mut s}) = schematic::from_b64(&*s) else {
c.reply("no schem!").await?;
return Ok(());
};
s.tags.insert("name".to_string(), name);
let mut o= DataWrite::default();
s.serialize(&mut o)?;
poise::send_reply(c, CreateReply::default().attachment(
CreateAttachment::bytes(o.consume(),"out.msch")
)).await?;
Ok(())
}
#[poise::command(
context_menu_command = "Render schematic",
install_context = "User",
interaction_context = "Guild|PrivateChannel"
)]
/// Renders schematic inside a message.
pub async fn render_message(c: Context<'_>, m: Message) -> Result<()> {
log(&c);poise::send_reply(
c, match schematic::from((&m.content, &m.attachments)).await {
Ok(Some(s)) =>
schematic::reply(
s,
&m.author_nick(c)
.await
.unwrap_or_else(|| m.author.name.clone()),
&m.author.avatar_url().unwrap_or(CAT.to_string()),
)
.await?,
Err(e) =>
CreateReply::default()
.content(format!("schematic error {e}")),
Ok(None) =>
CreateReply::default()
.content("no schem found")
.ephemeral(true)
}
)
.await?;
Ok(())
}
#[poise::command(
slash_command,
install_context = "Guild",
interaction_context = "Guild|PrivateChannel"
)]
/// Instructions on adding a schematic repository to YOUR server!
pub async fn schembrowser_instructions(c: Context<'_>) -> Result<()> {
log(&c);poise::send_reply(
c,
poise::CreateReply::default()
.content(include_str!("repo.md"))
.allowed_mentions(CreateAllowedMentions::default().empty_users().empty_roles()),
)
.await?;
Ok(())
}