Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
941 changes: 472 additions & 469 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

run-migration = pkgs.writeShellScriptBin "run-migration" (''diesel migration run'');

build-and-debug = pkgs.writeShellScriptBin "build-and-debug" (''cargo run'');

gen-up = pkgs.writeShellApplication {
name = "gen-up";

Expand Down Expand Up @@ -87,7 +85,7 @@
gen-up
gen-down

build-and-debug
(pkgs.writeShellScriptBin "run" (''cargo run''))

cmake # For songbird build
libopus # For songbird runtime
Expand Down
3 changes: 3 additions & 0 deletions migrations/2025-11-08-182549_saved-rolls/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
ALTER TABLE characters
DROP COLUMN saved_rolls;
3 changes: 3 additions & 0 deletions migrations/2025-11-08-182549_saved-rolls/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE characters
ADD COLUMN saved_rolls TEXT;
4 changes: 4 additions & 0 deletions src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ impl Character {
user_id: None,
name: None,

saved_rolls: None,

roll_server_id: None,

stat_block: None,
Expand Down Expand Up @@ -49,6 +51,8 @@ pub struct Character {
pub user_id: Option<String>,
pub name: Option<String>,

pub saved_rolls: Option<String>,

pub stat_block: Option<String>,
pub stat_block_hash: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ diesel::table! {
mana_readout_message_id -> Nullable<Text>,
stat_block_server_id -> Nullable<Text>,
roll_server_id -> Nullable<Text>,
saved_rolls -> Nullable<Text>,
}
}

Expand Down
127 changes: 116 additions & 11 deletions src/rpg/mir/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
pub mod saved_rolls;
pub mod spell_sheet;
pub mod stat_block;
pub mod web;

use crate::common::Data;

use lazy_static::lazy_static;
use poise::serenity_prelude::ButtonStyle;
use poise::serenity_prelude::ChannelId;
Expand All @@ -11,8 +14,10 @@ use poise::serenity_prelude::CreateEmbed;
use poise::serenity_prelude::CreateEmbedFooter;
use poise::serenity_prelude::CreateSelectMenu;
use poise::serenity_prelude::CreateSelectMenuOption;
use poise::serenity_prelude::EditMessage;
use poise::serenity_prelude::GuildId;
use poise::Command;
use poise::Modal;
use tokio::sync::Mutex;
use tokio::sync::MutexGuard;

Expand All @@ -25,20 +30,15 @@ use super::spells::SpellType;
use spell_sheet::SpellSheet;
use stat_block::StatBlock;

use std::borrow::Cow;
use std::collections::HashMap;
use std::f64;

use crate::common;
use crate::common::safe_to_number;
use crate::common::ButtonEventSystem;
use crate::common::Context;
use crate::common::Error;
use crate::db;
use crate::db::models::Character;
use crate::db::DbError;

use diesel::SqliteConnection;

use super::get_user_character;
use super::CharacterSheetable;
Expand All @@ -65,6 +65,8 @@ use event_handlers::UpdateStatusEventParams;
use event_handlers::ChangeManaEvent;
use event_handlers::ChangeManaEventParams;

type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;

pub fn register_events(event_system: &mut MutexGuard<ButtonEventSystem>) {
event_system.register_handler(RollEvent);
event_system.register_handler(ChangeCharacterEvent);
Expand Down Expand Up @@ -105,6 +107,76 @@ pub async fn pull_spellsheet(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}

#[derive(Debug, Modal)]
#[name = "Edit Message"] // Struct name by default
struct EditMessageModal {
#[name = "Message content:"]
#[paragraph]
message: String, // Option means optional input
}

#[derive(Debug, Modal)]
#[name = "Edit Saved Rolls"] // Struct name by default
struct EditSavedRollsModal {
#[name = "Saved Rolls"]
#[paragraph]
message: Option<String>, // Option means optional input
}

#[poise::command(slash_command)]
pub async fn edit_saved_rolls(ctx: ApplicationContext<'_>) -> Result<(), Error> {
let author = &ctx.author();

let user_id = author.id.get();

let user = db::users::get_or_create(user_id)?;

let mut char = if let Some(character_id) = user.selected_character {
Some(db::characters::get(character_id)?)
} else {
None
}
.ok_or(RpgError::NoCharacterSelected)?;

// let char = get_user_character(ctx.serenity_context());

let message_modal = EditSavedRollsModal {
message: char.saved_rolls,
};
Comment on lines +143 to +145
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: Medium

The modal uses the raw database value without any validation or sanitization. If the saved_rolls contain malformed data (e.g., lines without colons), the editing interface will display corrupted content, and the roll command will silently ignore those lines.

Code Suggestion:

// Add basic validation display
if let Some(rolls) = &char.saved_rolls {
    let valid_lines: Vec<&str> = rolls.lines()
        .filter(|line| line.contains(':'))
        .collect();
    message_modal.message = Some(valid_lines.join("\n"));
}


let data = Modal::execute_with_defaults(ctx, message_modal).await?;
if let Some(data) = data {
char.saved_rolls = data.message;
db::characters::update(&char)?;
}

Ok(())
}

// #[poise::command(context_menu_command = "Edit message")]
// pub async fn edit_character(
// ctx: ApplicationContext<'_>,

// msg: crate::serenity::Message,
// ) -> Result<(), Error> {
// let content = &msg.content;

// let message_modal = EditMessageModal {
// message: content.clone(),
// };

// let data = Modal::execute_with_defaults(ctx, message_modal).await?;
// if let Some(data) = data {
// if &data.message != content {
// msg.clone()
// .edit(ctx, EditMessage::default().content(&data.message))
// .await?;
// }
// }

// Ok(())
// }

static BAR_LENGTH: i32 = 16;

pub async fn generate_status_embed(
Expand Down Expand Up @@ -1143,6 +1215,18 @@ pub async fn roll_with_char_sheet(

match stat_block_result {
Ok(stat_block) => {
if let Some(custom_rolls) = &character.saved_rolls {
let custom_roll_map: std::collections::HashMap<_, _> = custom_rolls
.lines()
.filter_map(|line| line.split_once(':'))
.map(|(k, v)| (k.trim(), v.trim()))
.collect();

for (key, value) in &custom_roll_map {
str_replaced = str_replaced.replace(key, &value.to_string());
}
}

if let Some(stats_object) = stat_block
.stats
.as_ref()
Expand All @@ -1164,6 +1248,19 @@ pub async fn roll_with_char_sheet(
}
}
}
if let Some(special_stats_object) = stat_block
.special_stats
.as_ref()
.and_then(|special_stats| special_stats.as_object())
{
println!("Testing");
for (special_stat, value) in special_stats_object {
str_replaced = str_replaced.replace(special_stat, &value.to_string());
println!("special stat replaced: {special_stat}: {value}")
}
Comment on lines +1255 to +1260
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: High

Debug print statements left in production code create noise in logs and expose potentially sensitive roll calculation details. These should be removed or gated behind proper logging levels.

Suggested change
{
println!("Testing");
for (special_stat, value) in special_stats_object {
str_replaced = str_replaced.replace(special_stat, &value.to_string());
println!("special stat replaced: {special_stat}: {value}")
}
// Remove debug print statements

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you're able to reply

str_replaced is used later on, you're proposing removing the entire for loop and cutting out like 1/3 of the new feature

}

println!("{}", str_replaced);
}

Err(e) => {
Expand Down Expand Up @@ -1367,6 +1464,8 @@ pub async fn create_character(
user_id: Some(user_id.to_string()),
roll_server_id: roll_server_id,

saved_rolls: None,

stat_block: None,
stat_block_hash: None,

Expand Down Expand Up @@ -1688,12 +1787,16 @@ pub async fn pull_stats(ctx: Context<'_>) -> Result<(), Error> {
.await?
.ok_or(RpgError::NoCharacterSheet)?;

let reply = CreateReply::default().content(
stat_block
.sheet_info
.jsonified_message
.expect("Stat block should always generate json"),
);
let json = stat_block
.sheet_info
.jsonified_message
.expect("Stat block should always generate json");

let pretty_json =
serde_json::to_string_pretty(&serde_json::from_str::<serde_json::Value>(&json).unwrap())
.unwrap();
Comment on lines +1795 to +1797
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 | Confidence: High

Double-unwrap chain creates potential panic points. If the original JSON is invalid or pretty-printing fails, the command will crash. This is particularly risky since it involves user-generated content from character sheets.

Code Suggestion:

let pretty_json = match serde_json::from_str::<serde_json::Value>(&json) {
    Ok(value) => serde_json::to_string_pretty(&value).unwrap_or(json),
    Err(_) => json,
};


let reply = CreateReply::default().content(pretty_json);
msg.edit(ctx, reply).await?;

return Ok(());
Expand Down Expand Up @@ -1808,5 +1911,7 @@ pub fn commands() -> Vec<Command<crate::common::Data, crate::common::Error>> {
set_spells(),
level_up(),
roll(),
// edit_character(),
edit_saved_rolls(),
];
}
129 changes: 129 additions & 0 deletions src/rpg/mir/saved_rolls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// use std::fmt;

// use crate::common::Error;

// use crate::db::models::Character;

// use super::super::CharacterSheetable;
// use super::super::RpgError;
// use super::super::SheetInfo;

// use poise::serenity_prelude::Message;

// pub struct SavedRollSheet {
// pub sheet_info: SheetInfo,
// pub character_id: Option<i32>,
// pub saved_rolls: Option<Vec<SavedRoll>>,
// }

// #[derive(Clone)]
// pub struct SavedRoll {
// pub name: String,
// pub formula: String,
// }

// impl fmt::Display for SavedRollSheet {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// // if let (Some(name), Some(formula)) = (&self.name, &self.formula) {}

// write!(f, "I can't be bothered to do this right now")
// }
// }

// impl CharacterSheetable for SavedRollSheet {
// fn new() -> Self {
// return Self {
// character_id: None,

// saved_rolls: None,

// sheet_info: SheetInfo {
// original_message: None,
// jsonified_message: None,
// message_hash: None,
// changed: false,
// character: None,
// deserialized_message: None,
// },
// };
// }

// fn post_init(&mut self) -> Result<(), Error> {
// let deserialized_message = self
// .sheet_info
// .deserialized_message
// .as_ref()
// .expect("This should be set before calling post_init");

// Ok(())
// }

// fn update_character(&mut self) {
// let mut char = self
// .sheet_info
// .character
// .clone()
// .unwrap_or(Character::new_empty());

// char.stat_block = Some(
// self.sheet_info
// .jsonified_message
// .clone()
// .expect("Character sheet should always generate jsonified message"),
// );

// char.stat_block_hash = self.sheet_info.message_hash.clone();

// self.sheet_info.character = Some(char);
// }

// fn mut_sheet_info(&mut self) -> &mut SheetInfo {
// &mut self.sheet_info
// }
// fn sheet_info(&self) -> &SheetInfo {
// &self.sheet_info
// }

// fn get_previous_block(character: &Character) -> (Option<String>, Option<String>) {
// return (
// character.stat_block_hash.clone(),
// character.stat_block.clone(),
// );
// }

// async fn get_sheet_message(
// ctx: &poise::serenity_prelude::Context,
// character: &Character,
// ) -> Result<Message, Error> {
// if let (Some(channel_id_u64), Some(message_id_u64)) = (
// character.stat_block_channel_id.clone(),
// character.stat_block_message_id.clone(),
// ) {
// let channel_id = channel_id_u64.parse().expect("Invalid channel ID");
// let message_id = message_id_u64.parse().expect("Invalid message ID");

// let message = crate::common::fetch_message(&ctx, channel_id, message_id).await?;

// return Ok(message);
// }

// Err(Box::new(RpgError::NoCharacterSheet))
// }

// const PROMPT: &'static str = r#"
// You are a saved roll reading program.
// Following this prompt you will receive a key value pair list of roll formulas and their names.
// Use the following schema:
// {

// "my_custom_roll": (string),
// "my_other_roll": (string)

// }

// All keys should be lower case and spell corrected. Respond with only valid, minified json

// DO NOT USE BACKTICKS OR BACKSLASHES IN YOUR RESPONSE

// "#;
// }
Loading