kv, quotes, some other stuff

This commit is contained in:
Tanner Sommers 2024-11-25 13:33:33 -06:00
parent 008ff5b7cf
commit 7276f5922f
13 changed files with 352 additions and 6 deletions

15
Cargo.lock generated
View File

@ -1263,6 +1263,16 @@ version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.11" version = "0.2.11"
@ -2284,6 +2294,7 @@ dependencies = [
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"dotenv", "dotenv",
"libloading",
"poise", "poise",
"reqwest 0.12.9", "reqwest 0.12.9",
"serde", "serde",
@ -2422,6 +2433,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlformat", "sqlformat",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@ -2506,6 +2518,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror",
"time",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -2544,6 +2557,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror",
"time",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -2567,6 +2581,7 @@ dependencies = [
"serde", "serde",
"serde_urlencoded", "serde_urlencoded",
"sqlx-core", "sqlx-core",
"time",
"tracing", "tracing",
"url", "url",
] ]

View File

@ -7,7 +7,7 @@ edition = "2021"
dotenv = "0.15.0" dotenv = "0.15.0"
poise = "0.6.1" poise = "0.6.1"
serenity = "0.12.4" serenity = "0.12.4"
sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls-ring", "mysql" ] } sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls-ring", "mysql", "time" ] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
@ -17,3 +17,4 @@ chrono = "0.4.38"
chrono-tz = "0.10.0" chrono-tz = "0.10.0"
reqwest = "0.12.9" reqwest = "0.12.9"
serde_json = "1.0.133" serde_json = "1.0.133"
libloading = "0.8.5"

View File

@ -0,0 +1,5 @@
-- Add migration script here
CREATE TABLE kv_store (
`key` VARCHAR(255) NOT NULL PRIMARY KEY,
`value` TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1,11 @@
-- Add migration script here
CREATE TABLE quotes (
quote_id INT AUTO_INCREMENT PRIMARY KEY, -- Unique ID for each quote
user_id BIGINT NOT NULL, -- Discord user ID (64-bit)
username VARCHAR(255) NOT NULL, -- Username of the person who said the quote
quote TEXT NOT NULL, -- The quote itself
added_by BIGINT NOT NULL, -- Discord user ID of the person who added the quote
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the quote was added
INDEX (user_id), -- Index for efficient lookup by user_id
INDEX (added_by) -- Index for efficient lookup by added_by
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -1,7 +1,6 @@
use crate::{utils, Context, Error}; use crate::{utils, Context, Error};
use poise::CreateReply; use poise::CreateReply;
use serenity::all::{CreateAllowedMentions, CreateAttachment, Message}; use serenity::all::{CreateAllowedMentions, CreateAttachment, Message};
use serenity::builder::CreateMessage;
use serenity::model::prelude::UserId; use serenity::model::prelude::UserId;
use serenity::prelude::Mentionable; use serenity::prelude::Mentionable;

112
src/commands/eval.rs Normal file
View File

@ -0,0 +1,112 @@
use std::{
env, fs,
process::{Command, Output, Stdio},
time::{SystemTime, UNIX_EPOCH},
};
use crate::{Context, Error};
/// Evaluate rust code
#[poise::command(prefix_command)]
pub async fn eval(
ctx: Context<'_>,
#[description = "The code to run"] code: poise::CodeBlock,
) -> Result<(), Error> {
let authed_users = ctx.data().owners.clone();
// Check if the user is an owner
if !authed_users.contains(&ctx.author().id.into()) {
ctx.say(":x: You are not authorized to run this command")
.await?;
return Ok(());
}
ctx.say(":gear: Processing...").await?;
// Create a temporary directory for the file
let temp_dir = env::temp_dir();
let unique_id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let file_path = temp_dir.join(format!("eval_{}.rs", unique_id));
let executable_path = temp_dir.join(format!("eval_{}.out", unique_id));
// Write the code to a temporary file
if let Err(err) = fs::write(&file_path, code.code) {
ctx.say(format!("Error writing to temporary file: {}", err))
.await?;
return Ok(());
}
// Compile the Rust file and capture stderr
let compile_output: Result<Output, _> = Command::new("rustc")
.arg(&file_path)
.arg("-o")
.arg(&executable_path)
.stderr(Stdio::piped())
.output();
match compile_output {
Ok(output) if output.status.success() => {
ctx.say(
"<:success:1310650176037453834> Compilation successful. Running the executable...",
)
.await?;
// Run the compiled executable
let output = Command::new(&executable_path).output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
ctx.say(format!(
"**Output:**\n```\n{}\n```\n**Errors:**\n```\n{}\n```",
if stdout.trim().is_empty() {
"<no output>"
} else {
stdout.trim()
},
if stderr.trim().is_empty() {
"<no errors>"
} else {
stderr.trim()
}
))
.await?;
}
Err(err) => {
ctx.say(format!(
"<:error:1310650177056538655> Error running the executable: {}",
err
))
.await?;
}
}
}
Ok(output) => {
// If compilation failed, display the compiler error output
let stderr = String::from_utf8_lossy(&output.stderr);
ctx.say(format!(
"<:error:1310650177056538655> Compilation failed:\n```\n{}\n```",
stderr
))
.await?;
}
Err(err) => {
ctx.say(format!(
"<:error:1310650177056538655> Error invoking rustc: {}",
err
))
.await?;
}
}
// Clean up
let _ = fs::remove_file(&file_path);
let _ = fs::remove_file(&executable_path);
Ok(())
}

View File

@ -4,3 +4,5 @@ pub mod cta;
pub mod dog; pub mod dog;
pub mod profile; pub mod profile;
pub mod action; pub mod action;
pub mod eval;
pub mod quote;

91
src/commands/quote.rs Normal file
View File

@ -0,0 +1,91 @@
use crate::{Context, Error};
use poise::CreateReply;
use serenity::all::{CreateAllowedMentions, Message, User};
use sqlx::types::time::OffsetDateTime;
#[poise::command(context_menu_command = "Quote User")]
pub async fn quote_action(
ctx: Context<'_>,
#[description = "The target message to quote"] message: Message,
) -> Result<(), Error> {
let quote = crate::structs::quote::Quote {
quote_id: 0,
user_id: message.author.id.into(),
username: message.author.name.clone(),
quote: message.content.clone(),
added_by: ctx.author().id.into(),
added_at: OffsetDateTime::now_utc(),
};
if quote.user_id == quote.added_by {
ctx.say(":x: You can't quote yourself").await?;
return Ok(());
}
ctx.data().database_controller.quote_create(quote).await?;
ctx.say(":white_check_mark: Okay, I've immortalized that message for you :)")
.await?;
Ok(())
}
/// Get a random quote
#[poise::command(slash_command)]
pub async fn random_quote(ctx: Context<'_>) -> Result<(), Error> {
let quote = ctx.data().database_controller.quote_get_random().await?;
if let Some(quote) = quote {
ctx.send(
CreateReply::default()
.content(format!(
"{}: {}\nQuoted at: <t:{}:f> by <@{}>",
quote.username,
quote.quote,
quote.added_at.unix_timestamp(),
quote.added_by
))
.allowed_mentions(CreateAllowedMentions::new().empty_users()),
)
.await?;
} else {
ctx.say("No quotes found").await?;
}
Ok(())
}
/// Get quotes for a user, showing latest 10
#[poise::command(slash_command)]
pub async fn user_quotes(
ctx: Context<'_>,
#[description = "The user to get quotes for"] user: User,
) -> Result<(), Error> {
let quotes = ctx
.data()
.database_controller
.quote_get_by_user_id(user.id.into())
.await?;
if quotes.is_empty() {
ctx.say("No quotes found").await?;
return Ok(());
}
let mut response = String::new();
for quote in quotes {
response.push_str(&format!(
"{}: {}\nQuoted at: <t:{}:f> by <@{}>\n",
quote.username,
quote.quote,
quote.added_at.unix_timestamp(),
quote.added_by
));
}
ctx.send(
CreateReply::default()
.content(response)
.allowed_mentions(CreateAllowedMentions::new().empty_users()),
)
.await?;
Ok(())
}

View File

@ -1,3 +1,4 @@
use crate::structs::quote::Quote;
use crate::structs::user::User; use crate::structs::user::User;
use sqlx::MySqlPool; use sqlx::MySqlPool;
@ -99,4 +100,80 @@ impl DatabaseController {
Ok(()) Ok(())
} }
pub async fn kv_set(&self, key: &str, value: &str) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO kv_store (`key`, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?",
key,
value,
value
)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn kv_get(&self, key: &str) -> Result<Option<String>, sqlx::Error> {
let kv = sqlx::query!("SELECT * FROM kv_store WHERE `key` = ?", key)
.fetch_optional(&self.db)
.await?;
match kv {
Some(kv) => Ok(kv.value),
None => Ok(None),
}
}
pub async fn quote_create(&self, quote: Quote) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO quotes (user_id, username, quote, added_by) VALUES (?, ?, ?, ?)",
quote.user_id,
quote.username,
quote.quote,
quote.added_by
)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn quote_get_random(&self) -> Result<Option<Quote>, sqlx::Error> {
let quote = sqlx::query!("SELECT * FROM quotes ORDER BY RAND() LIMIT 1")
.fetch_optional(&self.db)
.await?;
match quote {
Some(quote) => Ok(Some(Quote {
quote_id: quote.quote_id,
user_id: quote.user_id,
username: quote.username,
quote: quote.quote,
added_by: quote.added_by,
added_at: quote.added_at.unwrap(),
})),
None => Ok(None),
}
}
pub async fn quote_get_by_user_id(&self, user_id: u64) -> Result<Vec<Quote>, sqlx::Error> {
let quote = sqlx::query!("SELECT * FROM quotes WHERE user_id = ?", user_id)
.fetch_all(&self.db)
.await?;
let mut quotes = Vec::new();
for q in quote {
quotes.push(Quote {
quote_id: q.quote_id,
user_id: q.user_id,
username: q.username,
quote: q.quote,
added_by: q.added_by,
added_at: q.added_at.unwrap(),
});
}
Ok(quotes)
}
} }

View File

@ -1,2 +1,2 @@
pub mod join;
pub mod db; pub mod db;
pub mod join;

View File

@ -4,6 +4,8 @@ mod handlers;
mod structs; mod structs;
mod utils; mod utils;
use std::sync::Arc;
use events::event_handler; use events::event_handler;
use handlers::db::DatabaseController; use handlers::db::DatabaseController;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -78,6 +80,7 @@ async fn init_sqlx() -> MySqlPool {
struct Data { struct Data {
database_controller: DatabaseController, database_controller: DatabaseController,
owners: Vec<u64>,
uptime: std::time::Instant, uptime: std::time::Instant,
config: Config, config: Config,
vouch_store: Mutex<Vec<Vouch>>, vouch_store: Mutex<Vec<Vouch>>,
@ -99,6 +102,7 @@ struct Channels {
main: u64, main: u64,
logs_public: u64, logs_public: u64,
logs_mod: u64, logs_mod: u64,
starboard: u64,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -136,6 +140,7 @@ async fn main() {
main: 0, main: 0,
logs_public: 0, logs_public: 0,
logs_mod: 0, logs_mod: 0,
starboard: 0,
}, },
roles: Roles { roles: Roles {
admin: 0, admin: 0,
@ -160,7 +165,9 @@ async fn main() {
let pool = init_sqlx().await; let pool = init_sqlx().await;
info!("initializing bot"); info!("initializing bot");
let intents = GatewayIntents::privileged(); let intents = GatewayIntents::privileged()
| GatewayIntents::GUILD_MEMBERS
| GatewayIntents::GUILD_MESSAGES;
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.options(poise::FrameworkOptions::<Data, Error> { .options(poise::FrameworkOptions::<Data, Error> {
@ -173,10 +180,22 @@ async fn main() {
commands::action::use_action_hug(), commands::action::use_action_hug(),
commands::action::use_action_kiss(), commands::action::use_action_kiss(),
commands::action::use_action_pat(), commands::action::use_action_pat(),
commands::eval::eval(),
commands::quote::quote_action(),
commands::quote::random_quote(),
commands::quote::user_quotes(),
], ],
event_handler: |ctx, event, framework, data| { event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data)) Box::pin(event_handler(ctx, event, framework, data))
}, },
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
std::time::Duration::from_secs(3600),
))),
case_insensitive_commands: true,
..Default::default()
},
..Default::default() ..Default::default()
}) })
.setup(|ctx, _ready, framework| { .setup(|ctx, _ready, framework| {
@ -188,6 +207,8 @@ async fn main() {
uptime: std::time::Instant::now(), uptime: std::time::Instant::now(),
config, config,
vouch_store: Mutex::new(Vec::new()), vouch_store: Mutex::new(Vec::new()),
// Sticks, Emi, Katie
owners: vec![1017196087276220447, 272871217256726531, 1033331958291369984],
}) })
}) })
}) })

View File

@ -1,2 +1,3 @@
pub mod vouch; pub mod quote;
pub mod user; pub mod user;
pub mod vouch;

11
src/structs/quote.rs Normal file
View File

@ -0,0 +1,11 @@
use sqlx::types::time::OffsetDateTime;
#[derive(Debug)]
pub struct Quote {
pub quote_id: i32,
pub user_id: i64,
pub username: String,
pub quote: String,
pub added_by: i64,
pub added_at: OffsetDateTime,
}