Updated spells schema

This commit is contained in:
Benjamin Sherriff
2023-10-03 21:00:14 -04:00
parent 16d8fa5af8
commit 75a71410a5
13 changed files with 890 additions and 143 deletions

1
.gitignore vendored
View File

@@ -8,4 +8,3 @@ audio/
logs/ logs/
settings.json settings.json
app/ app/
data/

37
data/spells/cantrips.json Normal file
View File

@@ -0,0 +1,37 @@
[
{
"name": "Acid Splash",
"school": "conjuration",
"level": 0,
"ritual": false,
"casting_time": {
"amount": 1,
"type": "action"
},
"range": {
"type": "point",
"amount": 60,
"unit": "feet"
},
"components": {
"verbal": true,
"somatic": true,
"material": false
},
"duration": {
"type": "instantaneous"
},
"classes": ["artificer", "sorcerer", "wizard"],
"sources": [
{
"source": "PHB",
"page": 211
}
],
"description": {
"entries": [
"You hurl a bubble of acid. Choose one creature within range, or choose two creatures within range that are within 5 feet of each other. A target must succeed on a Dexterity saving throw or take {@damage 1d6} acid damage.",
"This spell's damage increases by {@damage 1d6} when you reach 5th level ({@damage 2d6}), 11th level ({@damage 3d6}), and 17th level ({@damage 4d6})."]
}
}
]

0
data/spells/level_1.json Normal file
View File

View File

@@ -4,15 +4,29 @@ CREATE TABLE IF NOT EXISTS spells (
school TEXT NOT NULL, school TEXT NOT NULL,
level INTEGER NOT NULL, level INTEGER NOT NULL,
ritual BOOLEAN DEFAULT FALSE, ritual BOOLEAN DEFAULT FALSE,
casting_time TEXT NOT NULL, casting_time_amount INTEGER NOT NULL,
range TEXT NOT NULL, casting_time_unit TEXT NOT NULL,
saving_throw TEXT[],
attack_type TEXT,
damage_type TEXT,
conditions TEXT[],
range_type TEXT NOT NULL,
range_amount INTEGER,
range_unit TEXT,
area_type TEXT,
area_amount INTEGER,
area_unit TEXT,
components_verbal BOOLEAN DEFAULT FALSE, components_verbal BOOLEAN DEFAULT FALSE,
components_somatic BOOLEAN DEFAULT FALSE, components_somatic BOOLEAN DEFAULT FALSE,
components_material BOOLEAN DEFAULT FALSE, components_material BOOLEAN DEFAULT FALSE,
components_materials_needed TEXT, components_materials_needed TEXT,
duration TEXT NOT NULL, components_materials_cost INTEGER,
components_materials_consumed BOOLEAN DEFAULT FALSE,
duration_type TEXT NOT NULL,
duration_amount INTEGER,
duration_unit TEXT,
classes TEXT[] NOT NULL, classes TEXT[] NOT NULL,
sources TEXT[] NOT NULL, sources TEXT[] NOT NULL,
tags TEXT[], tags TEXT[],
description TEXT NOT NULL description JSONB NOT NULL
); );

View File

@@ -1,7 +1,3 @@
use std::error::Error;
use std::fmt;
use diesel::{prelude::*, insert_into}; use diesel::{prelude::*, insert_into};
use log::{error, debug, trace, warn}; use log::{error, debug, trace, warn};
@@ -13,12 +9,15 @@ use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverw
use serenity::prelude::*; use serenity::prelude::*;
use crate::db::{connection, messages::{MessageDB, NewMessageDB}}; use crate::db::{connection, messages::{MessageDB, NewMessageDB}};
use crate::error_handler::ServiceError;
pub struct OAI { pub struct OAI {
pub client: reqwest::Client, pub client: reqwest::Client,
pub base_url: String, pub base_url: String,
pub max_attempts: i64, pub max_attempts: i64,
pub token: String, pub token: String,
pub max_tokens: i64,
pub default_model: GPTModel,
pub max_context_questions: i64 pub max_context_questions: i64
} }
@@ -65,7 +64,7 @@ enum GPTRole {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
enum GPTModel { pub enum GPTModel {
#[serde(rename = "gpt-3.5-turbo")] #[serde(rename = "gpt-3.5-turbo")]
GPT35Turbo, GPT35Turbo,
#[serde(rename = "gpt-3.5-turbo-0613")] #[serde(rename = "gpt-3.5-turbo-0613")]
@@ -110,26 +109,18 @@ struct Choice {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct ResponseError { struct ResponseError {
code: Option<i64>, error: Option<ErrorDetails>,
message: Option<String>, message: Option<String>,
param: Option<String>, param: Option<String>,
#[serde(rename = "type")] #[serde(rename = "type")]
error_type: Option<String> error_type: Option<String>
} }
#[derive(Debug)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct OAIError { struct ErrorDetails {
pub message: String code: Option<String>
} }
impl fmt::Display for OAIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "OAIError: {}", self.message)
}
}
impl Error for OAIError {}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
enum ResponseEvent { enum ResponseEvent {
ChatCompletionResponse(ChatCompletionResponse), ChatCompletionResponse(ChatCompletionResponse),
@@ -137,7 +128,7 @@ enum ResponseEvent {
} }
impl OAI { impl OAI {
async fn get_request(&self, request: ChatCompletionRequest) -> Result<ChatCompletionResponse, OAIError> { async fn get_request(&self, request: ChatCompletionRequest) -> Result<ChatCompletionResponse, ServiceError> {
let uri = format!("{}/chat/completions", self.base_url); let uri = format!("{}/chat/completions", self.base_url);
let body = serde_json::to_string(&request).unwrap(); let body = serde_json::to_string(&request).unwrap();
trace!("Sending request to {}: {}", uri, body); trace!("Sending request to {}: {}", uri, body);
@@ -150,35 +141,39 @@ impl OAI {
.send() .send()
.await { .await {
Ok(r) => r, Ok(r) => r,
Err(err) => return Err(OAIError { Err(err) => return Err(ServiceError {
message: format!("Could not send request to OpenAI: {}", err), message: format!("Could not send request to OpenAI: {}", err),
status: 500
}) })
} }
.json::<Value>() .json::<Value>()
.await { .await {
Ok(r) => r, Ok(r) => r,
Err(err) => return Err(OAIError { Err(err) => return Err(ServiceError {
message: format!("Could not read response from OpenAI: {}", err) message: format!("Could not read response from OpenAI: {}", err),
status: 500
}) })
}; };
trace!("Received response from OpenAI: {:?}", value); trace!("Received response from OpenAI: {:?}", value);
// let response = match serde_json::from_value::<OAIResponseEvent>(value) { // let response = match serde_json::from_value::<ResponseEvent>(value) {
// Ok(r) => { // Ok(r) => {
// match r { // match r {
// OAIResponseEvent::OAIResponse(r) => r, // ResponseEvent::ChatCompletionResponse(r) => r,
// OAIResponseEvent::OAIError(e) => return Err(OAIError { message: e.message.unwrap_or("Unknown error".to_string()) }) // ResponseEvent::ResponseError(e) => return Err(ServiceError { message: e.message.unwrap_or("Unknown error".to_string()), status: 500 }),
// } // }
// }, // },
// Err(err) => return Err(OAIError { // Err(err) => return Err(ServiceError {
// message: format!("Could not parse response from OpenAI: {}", err) // message: format!("Could not parse response from OpenAI: {}", err),
// status: 500
// }) // })
// }; // };
let response = match serde_json::from_value::<ChatCompletionResponse>(value) { let response = match serde_json::from_value::<ChatCompletionResponse>(value) {
Ok(r) => r, Ok(r) => r,
Err(err) => return Err(OAIError { Err(err) => return Err(ServiceError {
message: format!("Could not parse response from OpenAI: {}", err) message: format!("Could not parse response from OpenAI: {}", err),
status: 500
}) })
}; };
@@ -209,20 +204,6 @@ pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) {
.limit(oai.max_context_questions) .limit(oai.max_context_questions)
.load(&mut connection); .load(&mut connection);
let previous_messages = match result {
Ok(r) => {
let mut previous_message = "".to_string();
for message in r {
previous_message = format!("{}You: {}\n Siren: {}\n", previous_message, message.request, message.response);
}
Some(ChatCompletionMessage { role: GPTRole::User, content: previous_message })
}
Err(err) => {
error!("Could not load previous messages: {}", err);
None
}
};
let mut messages = vec![ let mut messages = vec![
ChatCompletionMessage { ChatCompletionMessage {
role: GPTRole::System, role: GPTRole::System,
@@ -230,25 +211,34 @@ pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) {
}, },
]; ];
if let Some(mut previous) = previous_messages { match result {
previous.content = format!("{}You: {}\nSiren: ", previous.content, parsed_content); Ok(r) => {
messages.push(previous); for message in r {
} else { messages.push(
messages.push(ChatCompletionMessage { ChatCompletionMessage {
role: GPTRole::User, role: GPTRole::User,
content: format!("You: {}, Siren: ", parsed_content) content: format!("{}", message.request)
}); }
} );
messages.push(
let model = "gpt-3.5-turbo".to_string(); ChatCompletionMessage {
role: GPTRole::Assistant,
content: format!("{}", message.response)
}
);
}
},
Err(err) => error!("Could not load previous messages: {}", err)
};
messages.push(ChatCompletionMessage { role: GPTRole::User, content: parsed_content.clone() });
let request = ChatCompletionRequest { let request = ChatCompletionRequest {
model: GPTModel::GPT35Turbo, model: oai.default_model.clone(),
messages, messages,
temperature: Some(0.5), temperature: Some(0.5),
top_p: None, top_p: None,
n: None, n: None,
max_tokens: Some(1000), max_tokens: Some(oai.max_tokens),
presence_penalty: Some(0.6), presence_penalty: Some(0.6),
frequency_penalty: Some(0.0), frequency_penalty: Some(0.0),
user: Some(msg.author.name.clone()) user: Some(msg.author.name.clone())
@@ -289,7 +279,7 @@ pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) {
channel_id: response_channel.0 as i64, channel_id: response_channel.0 as i64,
user_id: author_id.0 as i64, user_id: author_id.0 as i64,
created: r.created, created: r.created,
model: &model, model: &serde_json::to_string(&r.model).unwrap(),
request: &parsed_content, request: &parsed_content,
response: &res, response: &res,
request_tags: vec![], request_tags: vec![],
@@ -305,7 +295,7 @@ pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) {
} }
Err(err) => { Err(err) => {
error!("Could not get response from OpenAI: {}", err.message); error!("Could not get response from OpenAI: {}", err.message);
err.message "There was an error processing your message. Please try again later.".to_string()
} }
}; };
debug!("Writing response: \"{}\"", response); debug!("Writing response: \"{}\"", response);

View File

@@ -0,0 +1,3 @@
mod model;
pub use model::*;

41
src/db/classes/model.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::str::FromStr;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub enum AbilityType {
Strength,
Dexterity,
Constitution,
Intelligence,
Wisdom,
Charisma
}
impl AbilityType {
pub fn to_string(&self) -> String {
match self {
AbilityType::Strength => "Strength".to_string(),
AbilityType::Dexterity => "Dexterity".to_string(),
AbilityType::Constitution => "Constitution".to_string(),
AbilityType::Intelligence => "Intelligence".to_string(),
AbilityType::Wisdom => "Wisdom".to_string(),
AbilityType::Charisma => "Charisma".to_string()
}
}
}
impl FromStr for AbilityType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Strength" => Ok(AbilityType::Strength),
"Dexterity" => Ok(AbilityType::Dexterity),
"Constitution" => Ok(AbilityType::Constitution),
"Intelligence" => Ok(AbilityType::Intelligence),
"Wisdom" => Ok(AbilityType::Wisdom),
"Charisma" => Ok(AbilityType::Charisma),
_ => Err(())
}
}
}

View File

@@ -0,0 +1,69 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub enum ConditionType {
Blinded,
Charmed,
Deafened,
Exhaustion,
Frightened,
Grappled,
Incapacitated,
Invisible,
Paralyzed,
Petrified,
Poisoned,
Prone,
Restrained,
Stunned,
Unconscious
}
impl ConditionType {
pub fn to_string(&self) -> String {
match self {
ConditionType::Blinded => "Blinded".to_string(),
ConditionType::Charmed => "Charmed".to_string(),
ConditionType::Deafened => "Deafened".to_string(),
ConditionType::Exhaustion => "Exhaustion".to_string(),
ConditionType::Frightened => "Frightened".to_string(),
ConditionType::Grappled => "Grappled".to_string(),
ConditionType::Incapacitated => "Incapacitated".to_string(),
ConditionType::Invisible => "Invisible".to_string(),
ConditionType::Paralyzed => "Paralyzed".to_string(),
ConditionType::Petrified => "Petrified".to_string(),
ConditionType::Poisoned => "Poisoned".to_string(),
ConditionType::Prone => "Prone".to_string(),
ConditionType::Restrained => "Restrained".to_string(),
ConditionType::Stunned => "Stunned".to_string(),
ConditionType::Unconscious => "Unconscious".to_string()
}
}
}
impl FromStr for ConditionType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Blinded" => Ok(ConditionType::Blinded),
"Charmed" => Ok(ConditionType::Charmed),
"Deafened" => Ok(ConditionType::Deafened),
"Exhaustion" => Ok(ConditionType::Exhaustion),
"Frightened" => Ok(ConditionType::Frightened),
"Grappled" => Ok(ConditionType::Grappled),
"Incapacitated" => Ok(ConditionType::Incapacitated),
"Invisible" => Ok(ConditionType::Invisible),
"Paralyzed" => Ok(ConditionType::Paralyzed),
"Petrified" => Ok(ConditionType::Petrified),
"Poisoned" => Ok(ConditionType::Poisoned),
"Prone" => Ok(ConditionType::Prone),
"Restrained" => Ok(ConditionType::Restrained),
"Stunned" => Ok(ConditionType::Stunned),
"Unconscious" => Ok(ConditionType::Unconscious),
_ => Err(())
}
}
}

View File

@@ -20,16 +20,30 @@ diesel::table! {
school -> Text, school -> Text,
level -> Integer, level -> Integer,
ritual -> Bool, ritual -> Bool,
casting_time -> Text, casting_time_amount -> Integer,
range -> Text, casting_time_unit -> Text,
saving_throw -> Nullable<Array<Text>>,
attack_type -> Nullable<Text>,
damage_type -> Nullable<Text>,
conditions -> Nullable<Array<Text>>,
range_type -> Text,
range_amount -> Nullable<Integer>,
range_unit -> Nullable<Text>,
area_type -> Nullable<Text>,
area_amount -> Nullable<Integer>,
area_unit -> Nullable<Text>,
components_verbal -> Bool, components_verbal -> Bool,
components_somatic -> Bool, components_somatic -> Bool,
components_material -> Bool, components_material -> Bool,
components_materials_needed -> Nullable<Text>, components_materials_needed -> Nullable<Text>,
duration -> Text, components_materials_cost -> Nullable<Integer>,
components_materials_consumed -> Nullable<Bool>,
duration_type -> Text,
duration_amount -> Nullable<Integer>,
duration_unit -> Nullable<Text>,
classes -> Array<Text>, classes -> Array<Text>,
sources -> Array<Text>, sources -> Array<Text>,
tags -> Array<Text>, tags -> Array<Text>,
description -> Text description -> Jsonb
} }
} }

View File

@@ -6,38 +6,40 @@ pub use routes::init_routes;
pub fn load_data() { pub fn load_data() {
let root_path = std::env::current_dir().unwrap(); let root_path = std::env::current_dir().unwrap();
let mut data_path = std::path::PathBuf::from(root_path); let files = [
data_path.push("data/spells.json"); "cantrips.json", "level_1.json", "level_2.json", "level_3.json", "level_4.json", "level_5.json", "level_6.json", "level_7.json", "level_8.json", "level_9.json"
match data_path.to_str() { ];
Some(path) => { let mut spells: Vec<Spell> = vec![];
log::debug!("Loading spells from {}", path); for file in files {
match std::fs::read_to_string(data_path) { let mut data_path = std::path::PathBuf::from(&root_path);
Ok(data) => { data_path.push(format!("data/spells/{}", file));
match serde_json::from_str::<serde_json::Value>(&data) { let path = data_path.to_str().unwrap();
Ok(json) => { match std::fs::read_to_string(path) {
match serde_json::from_value::<Vec<Spell>>(json) { Ok(data) => {
Ok(spells) => { log::debug!("Loading spells from {}", path);
let count = QuerySpell::get_count().unwrap(); match serde_json::from_str::<serde_json::Value>(&data) {
if count >= spells.len() as i64 { Ok(json) => {
log::warn!("Spell data is already loaded"); match serde_json::from_value::<Vec<Spell>>(json) {
return; Ok(mut new_spells) => spells.append(&mut new_spells),
} Err(err) => log::error!("Failed to parse spells data: {}", err)
for spell in spells { }
match InsertSpell::insert(spell.into()) { },
Ok(_) => {}, Err(err) => log::error!("Failed to parse spells data to value: {}", err)
Err(err) => log::error!("Failed to insert spell: {}", err) };
} },
} Err(err) => log::error!("Failed to read from {}: {}", file, err)
}, };
Err(err) => log::error!("Failed to parse spells data: {}", err) }
} let count = QuerySpell::get_count().unwrap();
}, if count >= spells.len() as i64 {
Err(err) => log::error!("Failed to parse spells data to value: {}", err) log::warn!("Spell data is already loaded");
}; return;
}, }
Err(err) => log::error!("Failed to read spells data: {}", err) for spell in spells {
}; let spell_name = spell.name.clone();
}, match InsertSpell::insert(spell.into()) {
None => log::error!("Failed to find spells data directory") Ok(_) => {},
Err(err) => log::error!("Failed to insert '{}' spell: {}", spell_name, err)
}
} }
} }

View File

@@ -1,7 +1,9 @@
use diesel::prelude::*; use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::{db::schema::spells, error_handler::ServiceError}; use diesel::prelude::*;
use serde::{Deserialize, Serialize, ser::SerializeMap};
use crate::{db::{schema::spells, classes::AbilityType, conditions::ConditionType}, error_handler::ServiceError};
#[derive(Queryable, QueryableByName)] #[derive(Queryable, QueryableByName)]
#[diesel(table_name = spells)] #[diesel(table_name = spells)]
@@ -11,17 +13,31 @@ pub struct QuerySpell {
pub school: String, pub school: String,
pub level: i32, pub level: i32,
pub ritual: bool, pub ritual: bool,
pub casting_time: String, pub casting_time_amount: i32,
pub range: String, pub casting_time_unit: String,
pub saving_throw: Option<Vec<String>>,
pub attack_type: Option<String>,
pub damage_type: Option<String>,
pub conditions: Option<Vec<String>>,
pub range_type: String,
pub range_amount: Option<i32>,
pub range_unit: Option<String>,
pub area_type: Option<String>,
pub area_amount: Option<i32>,
pub area_unit: Option<String>,
pub components_verbal: bool, pub components_verbal: bool,
pub components_somatic: bool, pub components_somatic: bool,
pub components_material: bool, pub components_material: bool,
pub components_materials_needed: Option<String>, pub components_materials_needed: Option<String>,
pub duration: String, pub components_materials_cost: Option<i32>,
pub components_materials_consumed: Option<bool>,
pub duration_type: String,
pub duration_amount: Option<i32>,
pub duration_unit: Option<String>,
pub classes: Vec<String>, pub classes: Vec<String>,
pub sources: Vec<String>, pub sources: Vec<String>,
pub tags: Vec<String>, pub tags: Vec<String>,
pub description: String pub description: serde_json::Value
} }
impl QuerySpell { impl QuerySpell {
@@ -63,17 +79,31 @@ pub struct InsertSpell {
pub school: String, pub school: String,
pub level: i32, pub level: i32,
pub ritual: bool, pub ritual: bool,
pub casting_time: String, pub casting_time_amount: i32,
pub range: String, pub casting_time_unit: String,
pub saving_throw: Option<Vec<String>>,
pub attack_type: Option<String>,
pub damage_type: Option<String>,
pub conditions: Option<Vec<String>>,
pub range_type: String,
pub range_amount: Option<i32>,
pub range_unit: Option<String>,
pub area_type: Option<String>,
pub area_amount: Option<i32>,
pub area_unit: Option<String>,
pub components_verbal: bool, pub components_verbal: bool,
pub components_somatic: bool, pub components_somatic: bool,
pub components_material: bool, pub components_material: bool,
pub components_materials_needed: Option<String>, pub components_materials_needed: Option<String>,
pub duration: String, pub components_materials_cost: Option<i32>,
pub components_materials_consumed: Option<bool>,
pub duration_type: String,
pub duration_amount: Option<i32>,
pub duration_unit: Option<String>,
pub classes: Vec<String>, pub classes: Vec<String>,
pub sources: Vec<String>, pub sources: Vec<String>,
pub tags: Vec<String>, pub tags: Vec<String>,
pub description: String pub description: serde_json::Value
} }
impl InsertSpell { impl InsertSpell {
@@ -90,50 +120,541 @@ impl InsertSpell {
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Spell { pub struct Spell {
pub name: String, pub name: String,
pub school: String, pub school: SchoolType,
pub level: i32, pub level: i32,
pub ritual: bool, pub ritual: bool,
pub casting_time: String, pub casting_time: CastingTime,
pub range: String, #[serde(skip_serializing_if = "Option::is_none")]
pub saving_throw: Option<Vec<AbilityType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attack_type: Option<SpellAttackType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub damage_type: Option<SpellDamageType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conditions: Option<Vec<ConditionType>>,
pub range: Range,
#[serde(skip_serializing_if = "Option::is_none")]
pub area: Option<Area>,
pub components: Components, pub components: Components,
pub duration: String, pub duration: Duration,
pub classes: Vec<String>, pub classes: Vec<String>,
pub sources: Vec<String>, pub sources: Vec<Source>,
pub tags: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub description: String pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Description>
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum SchoolType {
#[serde(rename = "abjuration")]
Abjuration,
#[serde(rename = "conjuration")]
Conjuration,
#[serde(rename = "divination")]
Divination,
#[serde(rename = "enchantment")]
Enchantment,
#[serde(rename = "evocation")]
Evocation,
#[serde(rename = "illusion")]
Illusion,
#[serde(rename = "necromancy")]
Necromancy,
#[serde(rename = "transmutation")]
Transmutation
}
impl SchoolType {
pub fn to_string(&self) -> String {
match self {
SchoolType::Abjuration => "abjuration".to_string(),
SchoolType::Conjuration => "conjuration".to_string(),
SchoolType::Divination => "divination".to_string(),
SchoolType::Enchantment => "enchantment".to_string(),
SchoolType::Evocation => "evocation".to_string(),
SchoolType::Illusion => "illusion".to_string(),
SchoolType::Necromancy => "necromancy".to_string(),
SchoolType::Transmutation => "transmutation".to_string()
}
}
}
impl FromStr for SchoolType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"abjuration" => Ok(SchoolType::Abjuration),
"conjuration" => Ok(SchoolType::Conjuration),
"divination" => Ok(SchoolType::Divination),
"enchantment" => Ok(SchoolType::Enchantment),
"evocation" => Ok(SchoolType::Evocation),
"illusion" => Ok(SchoolType::Illusion),
"necromancy" => Ok(SchoolType::Necromancy),
"transmutation" => Ok(SchoolType::Transmutation),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CastingTime {
pub amount: i32,
#[serde(rename = "type")]
pub casting_type: CastingType
}
#[derive(Debug, Serialize, Deserialize)]
pub enum CastingType {
#[serde(rename = "action")]
Action,
#[serde(rename = "bonus")]
BonusAction,
#[serde(rename = "reaction")]
Reaction,
#[serde(rename = "minutes")]
Minutes,
#[serde(rename = "hours")]
Hours
}
impl CastingType {
pub fn to_string(&self) -> String {
match self {
CastingType::Action => "action".to_string(),
CastingType::BonusAction => "bonus".to_string(),
CastingType::Reaction => "reaction".to_string(),
CastingType::Minutes => "minutes".to_string(),
CastingType::Hours => "hours".to_string()
}
}
}
impl FromStr for CastingType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"action" => Ok(CastingType::Action),
"bonus" => Ok(CastingType::BonusAction),
"reaction" => Ok(CastingType::Reaction),
"minutes" => Ok(CastingType::Minutes),
"hours" => Ok(CastingType::Hours),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum SpellAttackType {
#[serde(rename = "melee")]
Melee,
#[serde(rename = "ranged")]
Ranged,
}
impl SpellAttackType {
pub fn to_string(&self) -> String {
match self {
SpellAttackType::Melee => "melee".to_string(),
SpellAttackType::Ranged => "ranged".to_string()
}
}
}
impl FromStr for SpellAttackType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"melee" => Ok(SpellAttackType::Melee),
"ranged" => Ok(SpellAttackType::Ranged),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum SpellDamageType {
#[serde(rename = "acid")]
Acid,
#[serde(rename = "bludgeoning")]
Bludgeoning,
#[serde(rename = "cold")]
Cold,
#[serde(rename = "fire")]
Fire,
#[serde(rename = "force")]
Force,
#[serde(rename = "lightning")]
Lightning,
#[serde(rename = "necrotic")]
Necrotic,
#[serde(rename = "piercing")]
Piercing,
#[serde(rename = "poison")]
Poison,
#[serde(rename = "psychic")]
Psychic,
#[serde(rename = "radiant")]
Radiant,
#[serde(rename = "slashing")]
Slashing,
#[serde(rename = "thunder")]
Thunder
}
impl SpellDamageType {
pub fn to_string(&self) -> String {
match self {
SpellDamageType::Acid => "acid".to_string(),
SpellDamageType::Bludgeoning => "bludgeoning".to_string(),
SpellDamageType::Cold => "cold".to_string(),
SpellDamageType::Fire => "fire".to_string(),
SpellDamageType::Force => "force".to_string(),
SpellDamageType::Lightning => "lightning".to_string(),
SpellDamageType::Necrotic => "necrotic".to_string(),
SpellDamageType::Piercing => "piercing".to_string(),
SpellDamageType::Poison => "poison".to_string(),
SpellDamageType::Psychic => "psychic".to_string(),
SpellDamageType::Radiant => "radiant".to_string(),
SpellDamageType::Slashing => "slashing".to_string(),
SpellDamageType::Thunder => "thunder".to_string()
}
}
}
impl FromStr for SpellDamageType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"acid" => Ok(SpellDamageType::Acid),
"bludgeoning" => Ok(SpellDamageType::Bludgeoning),
"cold" => Ok(SpellDamageType::Cold),
"fire" => Ok(SpellDamageType::Fire),
"force" => Ok(SpellDamageType::Force),
"lightning" => Ok(SpellDamageType::Lightning),
"necrotic" => Ok(SpellDamageType::Necrotic),
"piercing" => Ok(SpellDamageType::Piercing),
"poison" => Ok(SpellDamageType::Poison),
"psychic" => Ok(SpellDamageType::Psychic),
"radiant" => Ok(SpellDamageType::Radiant),
"slashing" => Ok(SpellDamageType::Slashing),
"thunder" => Ok(SpellDamageType::Thunder),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Range {
#[serde(rename = "type")]
pub range_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Area {
#[serde(rename = "type")]
pub area_type: AreaType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>
}
#[derive(Debug, Serialize, Deserialize)]
pub enum AreaType {
#[serde(rename = "cone")]
Cone,
#[serde(rename = "cube")]
Cube,
#[serde(rename = "cylinder")]
Cylinder,
#[serde(rename = "line")]
Line,
#[serde(rename = "sphere")]
Sphere
}
impl AreaType {
pub fn to_string(&self) -> String {
match self {
AreaType::Cone => "cone".to_string(),
AreaType::Cube => "cube".to_string(),
AreaType::Cylinder => "cylinder".to_string(),
AreaType::Line => "line".to_string(),
AreaType::Sphere => "sphere".to_string()
}
}
}
impl FromStr for AreaType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cone" => Ok(AreaType::Cone),
"cube" => Ok(AreaType::Cube),
"cylinder" => Ok(AreaType::Cylinder),
"line" => Ok(AreaType::Line),
"sphere" => Ok(AreaType::Sphere),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Duration {
#[serde(rename = "type")]
pub duration_type: DurationType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>
}
#[derive(Debug, Serialize, Deserialize)]
pub enum DurationType {
#[serde(rename = "concentration")]
Concentration,
#[serde(rename = "instantaneous")]
Instantaneous,
#[serde(rename = "timed")]
Timed,
#[serde(rename = "dispelled")]
UntilDispelled,
#[serde(rename = "special")]
Special
}
impl DurationType {
pub fn to_string(&self) -> String {
match self {
DurationType::Concentration => "concentration".to_string(),
DurationType::Instantaneous => "instantaneous".to_string(),
DurationType::Timed => "timed".to_string(),
DurationType::UntilDispelled => "dispelled".to_string(),
DurationType::Special => "special".to_string()
}
}
}
impl FromStr for DurationType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"concentration" => Ok(DurationType::Concentration),
"instantaneous" => Ok(DurationType::Instantaneous),
"timed" => Ok(DurationType::Timed),
"dispelled" => Ok(DurationType::UntilDispelled),
"special" => Ok(DurationType::Special),
_ => Err(())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Source {
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i32>
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Description {
pub entries: Vec<Entry>
}
#[derive(Debug)]
pub struct Entry {
pub entry_type: String,
pub items: Vec<String>
}
impl<'de> Deserialize<'de> for Entry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => Ok(Entry {
entry_type: "string".to_string(),
items: vec![s]
}),
serde_json::Value::Object(o) => {
let entry_type = match o.get("type") {
Some(t) => match t.as_str() {
Some(s) => s.to_string(),
None => return Err(serde::de::Error::custom("Invalid entry type"))
},
None => return Err(serde::de::Error::custom("Missing entry type"))
};
let items = match o.get("items") {
Some(i) => match i.as_array() {
Some(a) => {
let mut items = Vec::new();
for item in a {
match item.as_str() {
Some(s) => items.push(s.to_string()),
None => return Err(serde::de::Error::custom("Invalid entry item"))
}
}
items
},
None => return Err(serde::de::Error::custom("Invalid entry items"))
},
None => return Err(serde::de::Error::custom("Missing entry items"))
};
Ok(Entry {
entry_type,
items
})
},
_ => Err(serde::de::Error::custom("Invalid entry"))
}
}
}
impl Serialize for Entry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer {
match self.entry_type.as_str() {
"string" => serializer.serialize_str(&self.items[0]),
_ => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", &self.entry_type)?;
map.serialize_entry("items", &self.items)?;
map.end()
}
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Components { pub struct Components {
pub verbal: bool, pub verbal: bool,
pub somatic: bool, pub somatic: bool,
pub material: bool, pub material: bool,
pub materials_needed: Option<String> #[serde(skip_serializing_if = "Option::is_none")]
pub materials_needed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub materials_cost: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub materials_consumed: Option<bool>
} }
impl From<QuerySpell> for Spell { impl From<QuerySpell> for Spell {
fn from(query: QuerySpell) -> Self { fn from(query: QuerySpell) -> Self {
return Self { return Self {
name: query.name, name: query.name,
school: query.school, school: match SchoolType::from_str(&query.school) {
Ok(school_type) => school_type,
Err(_) => {
log::error!("Failed to parse spell school type: {}", query.school);
SchoolType::Abjuration
}
},
level: query.level, level: query.level,
ritual: query.ritual, ritual: query.ritual,
casting_time: query.casting_time, casting_time: CastingTime {
range: query.range, amount: query.casting_time_amount,
casting_type: match CastingType::from_str(&query.casting_time_unit) {
Ok(casting_type) => casting_type,
Err(_) => {
log::error!("Failed to parse spell casting type: {}", query.casting_time_unit);
CastingType::Action
}
}
},
saving_throw: query.saving_throw.map(|saving_throw| saving_throw.iter().map(|ability_type| match AbilityType::from_str(&ability_type) {
Ok(ability_type) => ability_type,
Err(_) => {
log::error!("Failed to parse spell saving throw: {}", ability_type);
AbilityType::Strength
}
}).collect()),
attack_type: match query.attack_type {
Some(attack_type) => match SpellAttackType::from_str(&attack_type) {
Ok(attack_type) => Some(attack_type),
Err(_) => {
log::error!("Failed to parse spell attack type: {}", attack_type);
None
}
},
None => None
},
damage_type: query.damage_type.map(|damage_type| match SpellDamageType::from_str(&damage_type) {
Ok(damage_type) => damage_type,
Err(_) => {
log::error!("Failed to parse spell damage type: {}", damage_type);
SpellDamageType::Acid
}
}),
conditions: query.conditions.map(|conditions| conditions.iter().map(|condition_type| match ConditionType::from_str(&condition_type) {
Ok(condition_type) => condition_type,
Err(_) => {
log::error!("Failed to parse spell condition type: {}", condition_type);
ConditionType::Blinded
}
}).collect()),
range: Range {
range_type: query.range_type,
amount: query.range_amount,
unit: query.range_unit
},
area: match query.area_type {
Some(area_type) => Some(Area {
area_type: match AreaType::from_str(&area_type) {
Ok(area_type) => area_type,
Err(_) => {
log::error!("Failed to parse spell area type: {}", area_type);
AreaType::Cone
}
},
amount: query.area_amount,
unit: query.area_unit
}),
None => None
},
components: Components { components: Components {
verbal: query.components_verbal, verbal: query.components_verbal,
somatic: query.components_somatic, somatic: query.components_somatic,
material: query.components_material, material: query.components_material,
materials_needed: query.components_materials_needed materials_needed: query.components_materials_needed,
materials_cost: query.components_materials_cost,
materials_consumed: query.components_materials_consumed
},
duration: Duration {
duration_type: match DurationType::from_str(&query.duration_type) {
Ok(duration_type) => duration_type,
Err(_) => {
log::error!("Failed to parse spell duration type: {}", query.duration_type);
DurationType::Special
}
},
amount: query.duration_amount,
unit: query.duration_unit
}, },
duration: query.duration,
classes: query.classes, classes: query.classes,
sources: query.sources, sources: query.sources.iter().map(|source| Source {
tags: query.tags, source: source.to_string(),
description: query.description page: None
}).collect(),
tags: Some(query.tags),
description: match serde_json::from_value(query.description) {
Ok(description) => description,
Err(err) => {
log::error!("Failed to parse spell description: {}", err);
None
}
}
} }
} }
} }
@@ -142,20 +663,67 @@ impl Into<InsertSpell> for Spell {
fn into(self) -> InsertSpell { fn into(self) -> InsertSpell {
return InsertSpell { return InsertSpell {
name: self.name, name: self.name,
school: self.school, school: self.school.to_string(),
level: self.level, level: self.level,
ritual: self.ritual, ritual: self.ritual,
casting_time: self.casting_time, casting_time_amount: self.casting_time.amount,
range: self.range, casting_time_unit: self.casting_time.casting_type.to_string(),
saving_throw: match self.saving_throw {
Some(saving_throw) => Some(saving_throw.iter().map(|ability_type| ability_type.to_string()).collect()),
None => None
},
attack_type: match self.attack_type {
Some(attack_type) => Some(attack_type.to_string()),
None => None
},
damage_type: match self.damage_type {
Some(damage_type) => Some(damage_type.to_string()),
None => None
},
conditions: match self.conditions {
Some(conditions) => Some(conditions.iter().map(|condition_type| condition_type.to_string()).collect()),
None => None
},
range_type: self.range.range_type.to_string(),
range_amount: self.range.amount,
range_unit: self.range.unit,
area_type: match &self.area {
Some(area) => Some(area.area_type.to_string()),
None => None
},
area_amount: match &self.area {
Some(area) => area.amount,
None => None
},
area_unit: match &self.area {
Some(area) => match &area.unit {
Some(unit) => Some(unit.to_string()),
None => None
},
None => None
},
components_verbal: self.components.verbal, components_verbal: self.components.verbal,
components_somatic: self.components.somatic, components_somatic: self.components.somatic,
components_material: self.components.material, components_material: self.components.material,
components_materials_needed: self.components.materials_needed, components_materials_needed: self.components.materials_needed,
duration: self.duration, components_materials_cost: self.components.materials_cost,
components_materials_consumed: self.components.materials_consumed,
duration_type: self.duration.duration_type.to_string(),
duration_amount: self.duration.amount,
duration_unit: self.duration.unit,
classes: self.classes, classes: self.classes,
sources: self.sources, sources: self.sources.iter().map(|source| match source.page { Some(page) => format!("{} {}", source.source, page), None => source.source.to_string() }).collect(),
tags: self.tags, tags: match self.tags {
description: self.description Some(tags) => tags,
None => Vec::new()
},
description: match serde_json::to_value(self.description) {
Ok(description) => description,
Err(err) => {
log::error!("Failed to serialize spell description: {}", err);
serde_json::Value::Null
}
}
} }
} }
} }

View File

@@ -6,22 +6,22 @@ use std::fmt;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct ServiceError { pub struct ServiceError {
pub error_status_code: u16, pub status: u16,
pub error_message: String, pub message: String,
} }
impl ServiceError { impl ServiceError {
pub fn new(error_status_code: u16, error_message: String) -> ServiceError { pub fn new(error_status_code: u16, error_message: String) -> ServiceError {
ServiceError { ServiceError {
error_status_code, status: error_status_code,
error_message, message: error_message,
} }
} }
} }
impl fmt::Display for ServiceError { impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.error_message.as_str()) f.write_str(self.message.as_str())
} }
} }
@@ -42,13 +42,13 @@ impl From<DieselError> for ServiceError {
impl ResponseError for ServiceError { impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
let status_code = match StatusCode::from_u16(self.error_status_code) { let status_code = match StatusCode::from_u16(self.status) {
Ok(status_code) => status_code, Ok(status_code) => status_code,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR, Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}; };
let error_message = match status_code.as_u16() < 500 { let error_message = match status_code.as_u16() < 500 {
true => self.error_message.clone(), true => self.message.clone(),
false => "Internal server error".to_string(), false => "Internal server error".to_string(),
}; };

View File

@@ -20,6 +20,8 @@ use serenity::http::Http;
use serenity::prelude::*; use serenity::prelude::*;
use songbird::SerenityInit; use songbird::SerenityInit;
use crate::commands::oai::GPTModel;
mod commands; mod commands;
mod error_handler; mod error_handler;
mod db; mod db;
@@ -168,7 +170,15 @@ fn setup_discord_bot() {
Ok(token) => { Ok(token) => {
info!("Loaded OpenAI token"); info!("Loaded OpenAI token");
Handler { Handler {
oai: Some(commands::oai::OAI { client: reqwest::Client::new(), base_url: "https://api.openai.com/v1".to_string(), max_attempts: 5, token , max_context_questions: 15 }) oai: Some(commands::oai::OAI {
client: reqwest::Client::new(),
base_url: "https://api.openai.com/v1".to_string(),
max_attempts: 5,
token,
max_context_questions: 30,
max_tokens: 2048,
default_model: GPTModel::GPT35Turbo,
})
} }
} }
Err(err) => { Err(err) => {