Initial commit

This commit is contained in:
Histmy 2026-03-01 18:01:10 +01:00
commit 45556f2e3f
10 changed files with 2897 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2611
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "denim-bot"
version = "3002.0.0-alpha.1"
edition = "2024"
[dependencies]
denim-bot-macros = { path = "./denim-bot-macros" }
automod = "1.0.16"
diacritics = "0.2.2"
dotenvy = "0.15.7"
inventory = "0.3.22"
levenshtein = "1.0.5"
serenity = "0.12.5"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }

View File

@ -0,0 +1,11 @@
[package]
name = "denim-bot-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"

View File

@ -0,0 +1,24 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{ExprArray, ItemFn, parse_macro_input};
#[proc_macro_attribute]
pub fn bot_command(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let aliases = parse_macro_input!(attr as ExprArray);
let name = &input.sig.ident; // This is the function name
let expanded = quote! {
#input
inventory::submit! {
Komand {
name: stringify!(#name),
aliases: &#aliases,
implementation: KomandType::Fn(&|args| Box::pin(#name(args))),
}
}
};
expanded.into()
}

4
readme.md Normal file
View File

@ -0,0 +1,4 @@
# Denim bot v rustu
![:sjeta:](https://cdn.discordapp.com/emojis/623216247953424426.webp)
Certifikovaný to udělám™

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 150

129
src/main.rs Normal file
View File

@ -0,0 +1,129 @@
use crate::module::{Komand, KomandType, RunnableArgs, RunnableResult};
use std::collections::BTreeMap;
use std::env;
use levenshtein::levenshtein;
use serenity::async_trait;
use serenity::model::channel::Message;
use serenity::prelude::*;
mod module;
mod modules {
automod::dir!(pub "src/modules");
}
pub struct Handler {
commands: BTreeMap<String, KomandType>,
aliases: BTreeMap<String, String>,
}
const PREFIX: &str = "more"; // TODO: custom
#[async_trait]
impl EventHandler for Handler {
async fn message(&self, ctx: Context, msg: Message) {
println!("Received message: {}", msg.content);
let mut args = msg.content.split_whitespace();
let Some(prefix) = args.next() else {
return;
};
let prefix_diff = levenshtein(prefix, PREFIX);
if prefix_diff > 0 {
// TODO: run messagecreate event to check if vypni se
// TODO: make messageReply which checks antispam
if prefix_diff <= 1 && msg.author.id != ctx.cache.current_user().id {
msg.channel_id
.say(&ctx.http, format!("{}*", PREFIX))
.await
.expect("Failed to send message"); // TODO: ?
println!("poslano: {}*", PREFIX);
}
return;
}
let Some(command_with_diacritics) = args.next() else {
// TODO: run messagecreate event to check if vypni se
msg.channel_id.say(&ctx.http, "coe voe").await.expect("Failed to send message"); // TODO: ?
return;
};
let clean_command_name = diacritics::remove_diacritics(command_with_diacritics).to_lowercase(); // TODO: perhaps replace with something better
println!("Command name: {}", clean_command_name);
let command_name = {
if let Some(alias_target) = self.aliases.get(&clean_command_name) {
alias_target
} else {
&clean_command_name
}
};
let arguments: Vec<&str> = args.collect();
// TODO: run messagecreate event to check if vypni se
let command = self.commands.get(command_name);
// TODO: slash if not allowed in dms
if let Some(command) = command {
match command {
KomandType::Str(output) => {
msg.channel_id.say(&ctx.http, *output).await.expect("Failed to send message"); // TODO: ?
}
KomandType::Fn(func) => {
let params = RunnableArgs {
ctx: &ctx,
msg: &msg,
args: &arguments,
};
match func(&params).await {
RunnableResult::Nil => {}
RunnableResult::Str(output) => {
msg.channel_id.say(&ctx.http, output).await.expect("Failed to send message"); // TODO: ?
}
RunnableResult::String(output) => {
msg.channel_id.say(&ctx.http, output).await.expect("Failed to send message"); // TODO: ?
}
}
}
}
return;
}
msg.channel_id.say(&ctx.http, "neznam").await.expect("Failed to send message");
}
}
#[tokio::main]
async fn main() {
dotenvy::dotenv().expect("Failed to load .env file");
let mut commands = BTreeMap::new();
let mut aliases = BTreeMap::new();
for komand in inventory::iter::<Komand> {
let komand_name = komand.name.to_string();
commands.insert(komand_name.clone(), komand.implementation);
for alias in komand.aliases {
aliases.insert(alias.to_string(), komand_name.clone());
}
}
// Login with a bot token from the environment
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
// Set gateway intents, which decides what events the bot will be notified about
let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT;
// Create a new instance of the Client, logging in as a bot.
let mut client = Client::builder(&token, intents)
.event_handler(Handler { commands, aliases })
.await
.expect("Err creating client");
// Start listening for events by starting a single shard
if let Err(why) = client.start().await {
println!("Client error: {why:?}");
}
}

55
src/module.rs Normal file
View File

@ -0,0 +1,55 @@
use std::pin::Pin;
use serenity::all::{Context, Message};
pub struct Komand {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub implementation: KomandType,
}
inventory::collect!(Komand);
#[derive(Clone, Copy)]
pub enum KomandType {
Str(&'static str),
Fn(&'static (dyn for<'a> Fn(&'a RunnableArgs<'a>) -> Pin<Box<dyn Future<Output = RunnableResult> + Send + 'a>> + Send + Sync)),
}
pub struct RunnableArgs<'a> {
pub ctx: &'a Context,
pub msg: &'a Message,
pub args: &'a [&'a str],
}
pub enum RunnableResult {
Nil,
Str(&'static str),
String(String),
}
#[macro_export]
macro_rules! simple_command {
($name:literal, $aliases:expr, $output:literal) => {
inventory::submit! {
Komand {
name: $name,
aliases: &$aliases,
implementation: KomandType::Str($output),
}
}
};
($name:literal, $output:literal) => {
simple_command!($name, [], $output);
};
}
#[macro_export]
macro_rules! simple_commands {
( $( $name:literal => $output:literal ),* ) => {
$(
simple_command!($name, $output);
)*
};
}

47
src/modules/hello.rs Normal file
View File

@ -0,0 +1,47 @@
use serenity::all::{Context, OnlineStatus};
use crate::{
module::{Komand, KomandType, RunnableArgs, RunnableResult},
simple_command, simple_commands,
};
simple_command!("hello", ["hi", "hey"], "Hello, world!");
simple_command!("greet", "heyy");
simple_commands!(
"ping" => "Pong!",
"pong" => "Ping!"
);
fn change_status(ctx: &Context, status: OnlineStatus) -> &'static str {
ctx.set_presence(None, status);
return "ano pane";
}
#[denim_bot_macros::bot_command(["reknicau"])]
async fn pozdrav(args: &RunnableArgs<'_>) -> RunnableResult {
RunnableResult::String(format!("zdravim {}", args.args.get(0).unwrap_or(&"stranger")))
}
#[denim_bot_macros::bot_command(["onlajn"])]
async fn online(args: &RunnableArgs<'_>) -> RunnableResult {
return RunnableResult::Str(change_status(&args.ctx, OnlineStatus::Online));
}
#[denim_bot_macros::bot_command(["ajdl"])]
async fn idle(args: &RunnableArgs<'_>) -> RunnableResult {
return RunnableResult::Str(change_status(&args.ctx, OnlineStatus::Idle));
}
#[denim_bot_macros::bot_command([])]
async fn zareaguj(args: &RunnableArgs<'_>) -> RunnableResult {
args.msg.react(&args.ctx.http, '👋').await.expect("Failed to react to message");
return RunnableResult::Nil;
}
#[denim_bot_macros::bot_command([])]
async fn cekej(_: &RunnableArgs<'_>) -> RunnableResult {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
return RunnableResult::Str("hotovo");
}