html terminal
remove /draw_schematic
bendn 2023-08-24
parent f47225c · commit f254c72
-rw-r--r--src/bot/js.rs3
-rw-r--r--src/bot/maps.rs4
-rw-r--r--src/bot/mod.rs139
-rw-r--r--src/bot/schematic.rs123
-rw-r--r--src/bot/status.rs2
-rw-r--r--src/bot/usage.md4
-rw-r--r--src/bot/voting.rs2
7 files changed, 164 insertions, 113 deletions
diff --git a/src/bot/js.rs b/src/bot/js.rs
index 0b67e33..bd11c18 100644
--- a/src/bot/js.rs
+++ b/src/bot/js.rs
@@ -33,7 +33,8 @@ fn parse_js(from: &str) -> Result<String> {
required_permissions = "ADMINISTRATOR",
category = "Control",
track_edits,
- rename = "js"
+ rename = "js",
+ check = "crate::bot::in_guild"
)]
/// run arbitrary javascript
pub async fn run(
diff --git a/src/bot/maps.rs b/src/bot/maps.rs
index 89e363c..55b32da 100644
--- a/src/bot/maps.rs
+++ b/src/bot/maps.rs
@@ -45,7 +45,7 @@ pub async fn autocomplete<'a>(
.map(ToString::to_string)
}
-#[poise::command(slash_command, prefix_command, category = "Info", rename = "maps")]
+#[poise::command(slash_command, category = "Info", rename = "maps")]
/// lists the maps.
pub async fn list(ctx: Context<'_>) -> Result<()> {
let _ = ctx.defer_or_broadcast().await;
@@ -142,7 +142,7 @@ impl MapImage {
}
}
-#[poise::command(slash_command, prefix_command, category = "Info")]
+#[poise::command(slash_command, category = "Info")]
/// look at the current game.
pub async fn view(ctx: Context<'_>) -> Result<()> {
let _ = ctx.defer_or_broadcast().await;
diff --git a/src/bot/mod.rs b/src/bot/mod.rs
index 6912448..0dd57a5 100644
--- a/src/bot/mod.rs
+++ b/src/bot/mod.rs
@@ -12,7 +12,9 @@ use crate::webhook::Webhook;
use anyhow::Result;
use maps::Maps;
+use poise::serenity_prelude::GuildId;
use serenity::http::Http;
+use serenity::model::channel::Message;
use serenity::prelude::*;
use std::fmt::Write;
use std::fs::read_to_string;
@@ -44,15 +46,76 @@ macro_rules! send_ctx {
};
}
-#[cfg(not(debug_assertions))]
+const SOURCE_GUILD: u64 = 1003092764919091282;
+const ARROW_EMOJI: u64 = 1142290560275718194;
const PFX: &str = ">";
#[cfg(debug_assertions)]
-const PFX: &str = "-";
+const GUILD: u64 = SOURCE_GUILD;
+#[cfg(debug_assertions)]
+const CHANNEL: u64 = 1003092765581787279;
+#[cfg(not(debug_assertions))]
+const GUILD: u64 = 1110086242177142854;
+#[cfg(not(debug_assertions))]
+const CHANNEL: u64 = 1142100900442296441;
const SUCCESS: (u8, u8, u8) = (34, 139, 34);
const FAIL: (u8, u8, u8) = (255, 69, 0);
const DISABLED: (u8, u8, u8) = (112, 128, 144);
+pub async fn in_guild(ctx: Context<'_>) -> Result<bool> {
+ Ok(ctx.guild_id().map_or(false, |i| i.0 == GUILD))
+}
+
+pub async fn safe(m: &Message, c: &serenity::client::Context) -> String {
+ let mut result = m.content.clone();
+
+ for u in &m.mentions {
+ let mut at_distinct = String::with_capacity(33);
+ at_distinct.push('@');
+ at_distinct.push_str(&u.nick_in(c, GuildId(GUILD)).await.unwrap_or(u.name.clone()));
+
+ let mut m = u.mention().to_string();
+ if !result.contains(&m) {
+ m.insert(2, '!');
+ }
+ result = result.replace(&m, &at_distinct);
+ }
+
+ for id in &m.mention_roles {
+ let mention = id.mention().to_string();
+
+ if let Some(role) = id.to_role_cached(&c) {
+ result = result.replace(&mention, &["@", &role.name].concat());
+ } else {
+ result = result.replace(&mention, "@deleted-role");
+ }
+ }
+ result
+}
+
+pub async fn say(c: &serenity::client::Context, m: &Message, d: &Data) -> Result<()> {
+ let n = m
+ .author_nick(&c.http)
+ .await
+ .unwrap_or_else(|| m.author.name.replace("ggfenguin", "eris"));
+ for l in safe(m, c).await.lines() {
+ if send!(
+ d.stdin,
+ "say [royal] [coral][[[scarlet]{n}[coral]]:[white] {l}"
+ )
+ .is_err()
+ {
+ return Ok(());
+ };
+ }
+ m.react(
+ &c.http,
+ c.http.get_emoji(SOURCE_GUILD, ARROW_EMOJI).await.unwrap(),
+ )
+ .await?;
+ Ok(())
+}
+
pub struct Bot;
impl Bot {
pub async fn spawn(stdout: broadcast::Receiver<String>, stdin: broadcast::Sender<String>) {
@@ -77,8 +140,6 @@ impl Bot {
voting::create(),
voting::fixall(),
voting::list(),
- schematic::draw(),
- schematic::context_draw(),
start(),
end(),
help(),
@@ -90,35 +151,17 @@ impl Bot {
println!("bot ready");
}
poise::Event::Message { new_message } => {
- if [1142100900442296441, 1003092765581787279]
- .contains(new_message.channel_id.as_u64())
- && !new_message.content.starts_with('!')
- && !new_message.content.starts_with(PFX)
- && !new_message.author.bot
+ if new_message.content.starts_with('!')
+ || new_message.content.starts_with(PFX)
+ || new_message.author.bot
{
- if send!(
- d.stdin,
- "say [royal][] [scarlet][[{}]:[] {}",
- new_message
- .author_nick(&c.http)
- .await
- .unwrap_or_else(|| new_message.author.name.clone()),
- new_message.content_safe(&c.cache).replace('\n', "; ")
- )
- .is_err()
- {
- return Ok(());
- };
- new_message
- .react(
- &c.http,
- c.http
- .get_emoji(1003092764919091282, 1142290560275718194)
- .await
- .unwrap(),
- )
- .await
- .unwrap();
+ return Ok(());
+ }
+ if schematic::with(new_message, c).await?.is_break() {
+ return Ok(());
+ }
+ if CHANNEL == new_message.channel_id.0 {
+ say(c, new_message, d).await?;
}
}
_ => {}
@@ -141,7 +184,14 @@ impl Bot {
.setup(|ctx, _ready, framework| {
Box::pin(async move {
println!("registering");
- poise::builtins::register_globally(ctx, &framework.options().commands).await?;
+ poise::builtins::register_in_guild(
+ ctx,
+ &framework.options().commands[..18],
+ GuildId(GUILD),
+ )
+ .await?;
+ poise::builtins::register_globally(ctx, &framework.options().commands[18..])
+ .await?;
println!("registered");
Ok(Data {
stdin,
@@ -206,6 +256,7 @@ async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) {
#[poise::command(
prefix_command,
+ check = "crate::bot::in_guild",
required_permissions = "ADMINISTRATOR",
default_member_permissions = "ADMINISTRATOR",
category = "Control",
@@ -305,14 +356,18 @@ pub async fn help(
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<()> {
- poise::builtins::help(
- ctx,
- command.as_deref(),
- poise::builtins::HelpConfiguration {
- extra_text_at_bottom: "Mindustry server management bot",
- ..Default::default()
- },
- )
- .await?;
+ if in_guild(ctx).await.unwrap() {
+ poise::builtins::help(
+ ctx,
+ command.as_deref(),
+ poise::builtins::HelpConfiguration {
+ extra_text_at_bottom: "Mindustry server management bot",
+ ..Default::default()
+ },
+ )
+ .await?;
+ } else {
+ ctx.say(include_str!("usage.md")).await?;
+ }
Ok(())
}
diff --git a/src/bot/schematic.rs b/src/bot/schematic.rs
index 41e6cd8..370e2cb 100644
--- a/src/bot/schematic.rs
+++ b/src/bot/schematic.rs
@@ -1,48 +1,67 @@
-use super::{strip_colors, Context, SUCCESS};
use anyhow::{anyhow, Result};
-use mindus::data::{DataRead, DataWrite};
+use mindus::data::DataRead;
use mindus::*;
use oxipng::*;
use poise::serenity_prelude::*;
use regex::Regex;
-use std::borrow::Cow;
-use std::path::Path;
use std::sync::LazyLock;
+use std::{borrow::Cow, ops::ControlFlow};
+
+use super::{strip_colors, SUCCESS};
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(```)?(\n)?([^`]+)(\n)?(```)?").unwrap());
-#[poise::command(context_menu_command = "Render schematic", category = "Info")]
-/// draw schematic.
-pub async fn context_draw(ctx: Context<'_>, msg: Message) -> Result<()> {
- let _ = ctx.defer_or_broadcast().await;
-
- if let Some(a) = msg.attachments.get(0)
- && let Some(e) = Path::new(&a.filename).extension()
- && e == "msch" {
+pub async fn from_attachments(attchments: &[Attachment]) -> Result<Option<Schematic<'_>>> {
+ for a in attchments {
+ if a.filename.ends_with("msch") {
let s = a.download().await?;
let mut s = DataRead::new(&s);
- let s = Schematic::deserialize(&mut s).map_err(|e| anyhow!("invalid schematic: {e} with file {}", a.filename))?;
- return send(&ctx, &s, false).await;
+ let Ok(s) = Schematic::deserialize(&mut s) else {
+ continue;
+ };
+ return Ok(Some(s));
+ }
}
- draw_impl(ctx, &msg.content, false).await
+ Ok(None)
}
-#[poise::command(
- prefix_command,
- slash_command,
- category = "Info",
- rename = "draw_schematic",
- track_edits
-)]
-/// draw schematic.
-pub async fn draw(ctx: Context<'_>, schematic: String) -> Result<()> {
- let _ = ctx.defer_or_broadcast().await;
- draw_impl(ctx, &schematic, true).await
+pub async fn with(m: &Message, c: &serenity::client::Context) -> Result<ControlFlow<(), ()>> {
+ let send = |v| async move {
+ let p = to_png(&v);
+ let author = m.author_nick(c).await.unwrap_or(m.author.name.clone());
+ m.channel_id
+ .send_message(c, |m| {
+ m.add_file(AttachmentType::Bytes {
+ data: Cow::Owned(p),
+ filename: "image.png".to_string(),
+ })
+ .embed(|e| {
+ e.attachment("image.png");
+ if let Some(d) = v.tags.get("description") {
+ e.description(d);
+ }
+ e.title(strip_colors(v.tags.get("name").unwrap()))
+ .footer(|f| f.text(format!("requested by {author}",)))
+ .color(SUCCESS)
+ })
+ })
+ .await?;
+ anyhow::Ok(())
+ };
+
+ if let Ok(Some(v)) = from_attachments(&m.attachments).await {
+ send(v).await?;
+ return Ok(ControlFlow::Break(()));
+ }
+ if let Ok(v) = from_msg(&m.content) {
+ send(v).await?;
+ return Ok(ControlFlow::Break(()));
+ }
+ Ok(ControlFlow::Continue(()))
}
-async fn send(ctx: &Context<'_>, s: &Schematic<'_>, send_schematic: bool) -> Result<()> {
- let n = strip_colors(s.tags.get("name").unwrap());
+pub fn to_png(s: &Schematic<'_>) -> Vec<u8> {
let p = s.render();
let p = RawImage::new(
p.width(),
@@ -54,51 +73,23 @@ async fn send(ctx: &Context<'_>, s: &Schematic<'_>, send_schematic: bool) -> Res
p.buffer,
)
.unwrap();
- let p = 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::send_reply(*ctx, |m| {
- if send_schematic {
- let mut out = DataWrite::default();
- s.serialize(&mut out).unwrap();
- m.attachment(AttachmentType::Bytes {
- data: Cow::Owned(out.consume()),
- filename: "schem.msch".to_string(),
- });
- }
- m.attachment(AttachmentType::Bytes {
- data: Cow::Owned(p),
- filename: "image.png".to_string(),
- })
- .embed(|e| {
- if let Some(d) = s.tags.get("description") {
- e.description(d);
- }
- if send_schematic {
- e.attachment("schem.msch");
- }
- e.title(n).attachment("image.png").color(SUCCESS)
- })
+ 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)
})
- .await?;
- Ok(())
+ .unwrap()
}
-async fn draw_impl(ctx: Context<'_>, msg: &str, send_schematic: bool) -> Result<()> {
+pub fn from_msg<'l>(msg: &str) -> Result<Schematic<'l>> {
let schem_text = RE
.captures(msg)
.ok_or(anyhow!("couldnt find schematic"))?
.get(3)
.unwrap()
.as_str();
- let s = Schematic::deserialize_base64(schem_text)
- .map_err(|e| anyhow!("schematic deserializatiion failed: {e}"))?;
- send(&ctx, &s, send_schematic).await
+ Ok(Schematic::deserialize_base64(schem_text)?)
}
diff --git a/src/bot/status.rs b/src/bot/status.rs
index d8cd443..852042f 100644
--- a/src/bot/status.rs
+++ b/src/bot/status.rs
@@ -51,7 +51,7 @@ pub fn humanize_bytes<T: Into<Size>>(bytes: T) -> String {
[&result, SUFFIX[base.floor() as usize]].join(" ")
}
-#[poise::command(prefix_command, slash_command, category = "Info", rename = "status")]
+#[poise::command(slash_command, category = "Info", rename = "status")]
/// server status.
pub async fn command(ctx: Context<'_>) -> Result<()> {
let _ = ctx.defer_or_broadcast().await;
diff --git a/src/bot/usage.md b/src/bot/usage.md
new file mode 100644
index 0000000..625ec42
--- /dev/null
+++ b/src/bot/usage.md
@@ -0,0 +1,4 @@
+## Usage
+
+upload a file, with a msch extension (eg `24tpi_imp.msch`) and a schematic preview will be generated.
+you may instead upload a message containing a base64 encoded schematic. \ No newline at end of file
diff --git a/src/bot/voting.rs b/src/bot/voting.rs
index 2de5c96..61b789e 100644
--- a/src/bot/voting.rs
+++ b/src/bot/voting.rs
@@ -556,7 +556,7 @@ pub async fn fixall(ctx: Context<'_>) -> Result<()> {
// voters
-#[poise::command(prefix_command, slash_command, category = "Discord", rename = "votes")]
+#[poise::command(slash_command, category = "Discord", rename = "votes")]
pub async fn list(ctx: Context<'_>, #[description = "the vote title"] vote: String) -> Result<()> {
let vd = {
let buf = ctx.data().vote_data.lock().unwrap();