commit c8a846cd5af01303df0f28115b6125ba488b51a7 Author: evann Date: Sat Jan 15 17:38:28 2022 +0100 First Version of Drunk-Venti-Rust diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e828f8e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "drunk-venti-rust" +version = "1.0.0" +edition = "2021" +authors = ["Evann Regnault"] +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serenity = { version = "0.10.9", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "unstable_discord_api", "collector"] } +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +reqwest = "0.11.7" +serde_derive = "1.0.132" +serde = "1.0.132" +serde_json = {version = "1.0.73", features = ["indexmap"]} +levenshtein = "1.0.5" +regex = "1.5.4" +linked-hash-map = "0.5.4" +getset = "0.1.2" +mongodb = "2.1.0" +futures = "0.3.19" +rand = "0.8.4" +dotenv = "0.15.0" +dotenv_codegen = "0.15.0" \ No newline at end of file diff --git a/src/data/artifacts.rs b/src/data/artifacts.rs new file mode 100644 index 0000000..d3253e1 --- /dev/null +++ b/src/data/artifacts.rs @@ -0,0 +1,91 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; +use serenity::builder::CreateEmbed; +use crate::data::domains::Domain; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Set { + pub goblet: Option>, + pub plume: Option>, + pub circlet: Option>, + pub flower: Option>, + pub sands: Option> +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Artifact { + pub id: Box, + pub name: Box, + pub set_piece: Vec, + pub sets: Set, + pub bonuses: Vec>, + pub rarity: Vec, + pub domain: Option> +} + +impl Artifact{ + pub async fn get(artifact: &str) -> Artifact { + let url = format!("http://localhost:3000/api/artifacts/{}", artifact); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/artifacts"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub(crate) async fn search(artifact: &str) -> Vec { + let url = format!("http://localhost:3000/api/artifacts/search/{}", artifact); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub async fn to_embed(&self) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + + embed.title(format!("{} | {}", self.name, ":star:".repeat(self.rarity.get(0).expect("").to_owned() as usize))); + + embed.thumbnail(format!("https://raw.githubusercontent.com/MadeBaruna/paimon-moe/main/static/images/artifacts/{}_circlet.png", self.id)); + + for i in 0..self.set_piece.len(){ + embed.field(format!("{}-Pieces", self.set_piece.get(i).expect("No such piece set")), + format!("{}", self.bonuses.get(i).expect("Mo such effects")), + false + ); + } + + match &self.domain { + Some(d) => { + let domain = Domain::get(d).await; + embed.field("Domain", domain.name(), true); + } + _ => {} + } + + return embed; + } + + #[allow(dead_code)] + pub fn id(&self) -> &str { + return self.id.as_ref() + } + + #[allow(dead_code)] + pub fn name(&self) -> &str { + return self.name.as_ref() + } +} + +pub async fn test_artifacts() { + for a in Artifact::get_all().await { + println!("{}", a); + let arti = Artifact::get(&a).await; + println!("Name : {}\nCirclet : {}\nBonuses {:?}", arti.name, arti.sets.goblet.unwrap_or(Box::from("")), arti.bonuses); + } + +} diff --git a/src/data/builds.rs b/src/data/builds.rs new file mode 100644 index 0000000..401236b --- /dev/null +++ b/src/data/builds.rs @@ -0,0 +1,61 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RoleWeapon { + pub id: Box, + pub refine: Option> +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RoleStat { + pub sands: Box, + pub goblet: Box, + pub circlet: Box +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Role{ + pub name: Box, + pub recommended: bool, + pub weapons: Vec, + pub artifacts: Vec>>, + pub main_stats: RoleStat, + pub sub_stats: Vec>, + pub talent: Vec>, + pub tip: Box, + pub note: Box, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Builds { + pub roles: Vec +} + +impl Builds { + pub(crate) async fn get(build: &str) -> Builds { + let url = format!("http://localhost:3000/api/builds/{}", build); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/builds"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } +} + + +pub async fn test_builds() { + for a in Builds::get_all().await { + println!("------------------"); + let builds = Builds::get(&a).await; + println!("Roles for {} : {:?}\n", a, builds.roles.into_iter().map(|x| x.name).collect::>>()); + } + +} \ No newline at end of file diff --git a/src/data/characters.rs b/src/data/characters.rs new file mode 100644 index 0000000..c6a027f --- /dev/null +++ b/src/data/characters.rs @@ -0,0 +1,76 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; +use crate::data::builds::{Role}; +use crate::data::elements::Element; +use crate::data::items::Item; +use crate::data::shared_structs::{Ascension, WeaponType}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CharacterStats { + pub hp: u64, + pub atk: u64, + pub def: u64 +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CharacterMaterials { + pub book: Vec, + pub material: Vec, + pub boss: Item +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Character { + pub name: Box, + pub id: Box, + pub rarity: u8, + pub element: Element, + pub weapon: WeaponType, + pub ascension: Vec, + pub stats: CharacterStats, + pub material: CharacterMaterials, + pub builds: Vec +} + +impl Clone for Character { + fn clone(&self) -> Self { + let x = serde_json::to_string(self).expect("str"); + serde_json::from_str::(x.as_str()).expect("An error occurred") + } +} + + +impl Character { + #[allow(dead_code)] + pub(crate) async fn get(character: &str) -> Character { + let url = format!("http://localhost:3000/api/characters/{}", character); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/characters"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub(crate) async fn search(character: &str) -> Vec { + let url = format!("http://localhost:3000/api/characters/search/{}", character); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } +} + +#[allow(dead_code)] +pub async fn test_character () { + for a in Character::get_all().await { + println!("{}", a); + let char = Character::get(&a).await; + println!("Name : {}", char.name); + } +} \ No newline at end of file diff --git a/src/data/domains.rs b/src/data/domains.rs new file mode 100644 index 0000000..c3d4b13 --- /dev/null +++ b/src/data/domains.rs @@ -0,0 +1,70 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DomainMonster { + pub id: Box, + pub name: Box, + pub count: u8, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DomainReward { + pub adventure_exp: Box, + pub mora: Box, + pub friendship_exp: Box, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DomainDifficulty { + pub s: u64, + pub id: Box, + pub name: Box, + pub ar: u8, + pub level: u8, + pub reward: DomainReward, + pub monsters: Vec, + pub disorder: Vec> +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Domain { + pub name: Box, + pub domains: Vec, + pub artifacts: Vec>, +} + +impl Domain { + #[allow(dead_code)] + pub async fn get(domain: &str) -> Domain { + let url = format!("http://localhost:3000/api/domains/{}", domain); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/domains"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub fn name(&self) -> String { + return self.name.to_string(); + } +} + + +#[allow(dead_code)] +pub async fn test_domains () { + for a in Domain::get_all().await { + println!("{}", a); + let domain = Domain::get(&a).await; + println!("Name : {}", domain.name); + } +} \ No newline at end of file diff --git a/src/data/elements.rs b/src/data/elements.rs new file mode 100644 index 0000000..4fce6c4 --- /dev/null +++ b/src/data/elements.rs @@ -0,0 +1,32 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Element { + pub id: Box, + pub name: Box, + pub simple_name: Box, + pub color: u64 +} + +impl Element{ + async fn get(element: &str) -> Element { + let url = format!("http://localhost:3000/api/elements/{}", element); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/elements"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } +} + +pub async fn test_elem() { + println!("All elems : {:?}", Element::get_all().await); + + let geo = Element::get("geo").await; + println!("Name : {}\nColor : #{:x}", geo.name, geo.color); +} \ No newline at end of file diff --git a/src/data/events.rs b/src/data/events.rs new file mode 100644 index 0000000..55d958a --- /dev/null +++ b/src/data/events.rs @@ -0,0 +1,62 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Event { + pub name: Box, + pub image: Option>, + pub start: Box, + pub end: Box, + pub url: Option>, + pub start_timestamp: u64, + pub end_timestamp: u64, + pub show_on_home: Option +} + +impl Clone for Event { + fn clone(&self) -> Self { + let x = serde_json::to_string(&self).expect(""); + serde_json::from_str::(x.as_str()).expect("") + } +} + + + +impl Event { + #[allow(dead_code)] + pub(crate) async fn get_current() -> Vec { + let url = format!("http://localhost:3000/api/events/current"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub(crate) async fn get_upcoming() -> Vec { + let url = format!("http://localhost:3000/api/events/upcoming"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + async fn get_all() -> Vec { + let url = format!("http://localhost:3000/api/events"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } +} + +#[allow(dead_code)] +pub async fn test_events() { + let upcoming = Event::get_upcoming().await; + println!("UPCOMING EVENTS\n--------"); + for event in upcoming { + println!("Name : {}", event.name); + } + + let current = Event::get_current().await; + println!("CURRENT EVENTS\n--------"); + for event in current { + println!("Name : {}", event.name); + } +} \ No newline at end of file diff --git a/src/data/items.rs b/src/data/items.rs new file mode 100644 index 0000000..b68c08c --- /dev/null +++ b/src/data/items.rs @@ -0,0 +1,43 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Item { + pub id: Box, + pub name: Box, + pub day: Option>>, + pub rarity: Option, + pub parent: Option> +} + +impl Item{ + async fn get(item: &str) -> Item { + let url = format!("http://localhost:3000/api/items/{}", item); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/items"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + async fn search(item: &str) -> Vec { + let url = format!("http://localhost:3000/api/items/search/{}", item); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } +} + +pub async fn test_items() { + println!("List all items : {:?}", Item::get_all().await); + + let item = Item::get("relic_from_guyun").await; + println!("Name : {}\nDays : {:?}", item.name, item.day.unwrap_or(vec![])); + + let item = Item::get("mora").await; + println!("Name : {}\nDays : {:?}", item.name, item.day.unwrap_or(vec![])); +} \ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..56e1676 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,20 @@ +pub mod elements; +pub mod artifacts; +pub mod items; +pub mod weapons; +pub mod builds; +pub mod characters; +pub mod events; +pub mod shared_structs; +pub mod domains; + +#[allow(dead_code)] +pub async fn test() { + elements::test_elem().await; + artifacts::test_artifacts().await; + items::test_items().await; + weapons::test_weapons().await; + builds::test_builds().await; + characters::test_character().await; + events::test_events().await; +} \ No newline at end of file diff --git a/src/data/shared_structs.rs b/src/data/shared_structs.rs new file mode 100644 index 0000000..8f6f3ce --- /dev/null +++ b/src/data/shared_structs.rs @@ -0,0 +1,24 @@ +use serde_derive::{Serialize, Deserialize}; +use crate::data::items::Item; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WeaponType { + pub id: Box, + pub name: Box +} + + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AscensionItem { + pub item: Item, + pub amount: u32 +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Ascension { + pub items: Vec, + pub mora: u64 +} \ No newline at end of file diff --git a/src/data/weapons.rs b/src/data/weapons.rs new file mode 100644 index 0000000..e566ba3 --- /dev/null +++ b/src/data/weapons.rs @@ -0,0 +1,121 @@ +use reqwest::Url; +use serde_derive::{Serialize, Deserialize}; +use serenity::builder::CreateEmbed; +use regex::Regex; +use crate::data::shared_structs::Ascension; +use crate::data::shared_structs::WeaponType; + + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Skill { + pub name: Option>, + pub description: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Secondary { + pub name: Option>, + pub stats: Option>>, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Extras { + pub id: Box, + pub name: Box, + #[serde(rename = "type")] + pub weapon_type: Box, + pub rarity: u8, + pub description: Box, + pub skill: Skill, + pub secondary: Secondary, + pub atk: Vec>, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Weapon { + pub name: Box, + pub id: Box, + pub rarity: u8, + pub atk: u64, + pub secondary: Box, + #[serde(rename = "type")] + pub weapon_type: WeaponType, + pub source: Box, + pub ascension: Vec, + pub extras: Extras, +} + +impl Weapon { + #[allow(dead_code)] + pub async fn get(weapon: &str) -> Weapon { + let url = format!("http://localhost:3000/api/weapons/{}", weapon); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub async fn get_all() -> Vec> { + let url = format!("http://localhost:3000/api/weapons"); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub async fn search(weapon: &str) -> Vec { + let url = format!("http://localhost:3000/api/weapons/search/{}", weapon); + let url = Url::parse(&*url).expect("Can't convert url"); + return reqwest::get(url).await.expect("Can't access Url").json::>().await.expect("Wrong json format"); + } + + #[allow(dead_code)] + pub fn name(&self) -> &str { + return &self.name; + } + + #[allow(dead_code)] + pub fn id(&self) -> &str { + return &self.id; + } + + #[allow(dead_code)] + pub fn to_embed(&self) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + embed.title(format!("{} | {}", self.name, ":star:".repeat(self.rarity as usize))); + + embed.description(format!("{}", self.extras.description)); + + embed.thumbnail(format!("https://raw.githubusercontent.com/MadeBaruna/paimon-moe/main/static/images/weapons/{}.png", self.id)); + + // Fields + match self.extras.skill.name.as_ref() { + Some(t) => { + let re = Regex::new("(<|]*)?>").expect("Unknown regex"); + embed.field(format!("Passive : {}", t), + format!("{}", + re.replace_all(self.extras.skill.description.as_ref() + .unwrap_or(&Box::from("")) + .to_string().as_str(), "**")).replace("\\n","\n"), + false); + } + _ => {} + } + embed.field("Main Stat", format!("{}", self.secondary), true); + embed.field("Source", format!("{}", self.source), true); + embed.field("\u{200b}", "\u{200b}", true); + embed.field("Type", format!("{}", self.weapon_type.name), true); + embed.field("Ascension Item", format!("{}", self.ascension.get(0).expect("").items.get(0).expect("").item.name), true); + embed.field("\u{200b}", "\u{200b}", true); + return embed; + } +} + +pub async fn test_weapons() { + for w in Weapon::get_all().await { + println!("------------------"); + println!("{}", w); + } +} \ No newline at end of file diff --git a/src/interactions/genshin/artifacts.rs b/src/interactions/genshin/artifacts.rs new file mode 100644 index 0000000..0b03d90 --- /dev/null +++ b/src/interactions/genshin/artifacts.rs @@ -0,0 +1,44 @@ +use linked_hash_map::LinkedHashMap; +use serenity::client::Context; +use serenity::model::interactions::{InteractionApplicationCommandCallbackDataFlags, InteractionResponseType}; +use serenity::model::interactions::application_command::{ApplicationCommandInteraction, ApplicationCommandInteractionDataOption}; +use serenity::model::interactions::message_component::{MessageComponentInteraction}; +use crate::data::artifacts::Artifact; +use crate::interactions::utils::create_action_row_basic; + +pub async fn genshin_artifact_interaction(ctx: &Context, command: &ApplicationCommandInteraction, opt: &ApplicationCommandInteractionDataOption) { + + let weapon = opt.options.get(0).expect("No argument for command Genshin artifact") + .value.as_ref().expect("").as_str().expect("Not a string"); + + let artifacts = Artifact::search(weapon).await; + let mut artifact_list: LinkedHashMap = LinkedHashMap::new(); + + for a in artifacts { + artifact_list.insert(a.id().to_string(), a.name().to_string()); + } + + let ar = create_action_row_basic(artifact_list, "artifact"); + + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Select an Artifact") + .components(|c| c.add_action_row(ar)) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.expect("Message didn't got sent"); +} + + +pub async fn show_artifact_embed(ctx: &Context, command: &MessageComponentInteraction, artifact_name: String){ + let artifact = Artifact::get(artifact_name.as_str()).await; + let embed = artifact.to_embed().await; + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.embeds(vec!(embed).into_iter()) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.expect("Interaction failed"); +} \ No newline at end of file diff --git a/src/interactions/genshin/build.rs b/src/interactions/genshin/build.rs new file mode 100644 index 0000000..3e89147 --- /dev/null +++ b/src/interactions/genshin/build.rs @@ -0,0 +1,416 @@ +use std::borrow::{Borrow}; +use linked_hash_map::LinkedHashMap; +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed}; +use serenity::client::Context; +use serenity::model::interactions::{InteractionApplicationCommandCallbackDataFlags, InteractionResponseType}; +use serenity::model::interactions::application_command::{ApplicationCommandInteraction, ApplicationCommandInteractionDataOption}; +use serenity::model::interactions::message_component::{ButtonStyle, MessageComponentInteraction}; +use crate::data::artifacts::Artifact; +use crate::data::builds::{Builds, Role}; +use crate::data::characters::Character; +use crate::data::weapons::Weapon; +use crate::interactions::utils::create_action_row_basic; + +pub async fn genshin_build_interaction(ctx: &Context, command: &ApplicationCommandInteraction, opt: &ApplicationCommandInteractionDataOption) { + let char = opt.options.get(0).expect("No argument for command Genshin builds") + .value.as_ref().expect("").as_str().expect("Not a string"); + + let characters = Character::search(char).await; + let mut character_list: LinkedHashMap = LinkedHashMap::new(); + + for c in characters { + character_list.insert(c.id.to_string(), c.name.to_string()); + } + + let ar = create_action_row_basic(character_list, "build"); + + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Select a Character") + .components(|c| c.add_action_row(ar)) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.expect("Message didn't got sent"); +} + +fn is_menu(arg: &str) -> bool { + match arg { + "home" | "artifacts" | "weapons" | "notes" => true, + _ => false + } +} + +pub async fn build_interact(ctx: &Context, interaction: &MessageComponentInteraction, arguments: String) { + let mut args = arguments.split("_"); + let command = args.next(); + + + match command.expect("") { + c if is_menu(c) => { + let role_index = args.next().expect("").parse::().expect(""); + let character_name = args.collect::>().join("_"); + let character = Character::get(character_name.as_str()).await; + let builds = Builds::get(character_name.as_str()).await; + let role = builds.roles.get(role_index).expect(""); + let ar = nav_button(character_name, role_index); + + let embed = match c { + "home" => home_embed(role, character).await, + "artifacts" => artifact_embed(role, character).await, + "weapons" => weapons_embed(role, character).await, + "notes" => note_embed(role, character).await, + _ => CreateEmbed::default() + }; + + interaction.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.add_embed(embed) + .components(|c| c.add_action_row(ar)) + }) + }).await.expect("Can't send response"); + } + _ => { + let mut character = args.collect::>().join("_"); + character = [command.expect(""), character.as_str()].join("_"); + character = character.strip_suffix("_").unwrap_or(character.as_str()).parse().unwrap(); + build_select(&ctx, &interaction, character).await; + } + } +} + +fn nav_button(character_name: String, role_index: usize) -> CreateActionRow { + let mut ar = CreateActionRow::default(); + + let mut btn_home = CreateButton::default(); + btn_home + .custom_id(format!("build_home_{}_{}", role_index, character_name)) + .style(ButtonStyle::Primary) + .label("Home"); + + let mut btn_artifacts = CreateButton::default(); + btn_artifacts + .custom_id(format!("build_artifacts_{}_{}", role_index, character_name)) + .style(ButtonStyle::Primary) + .label("Artifacts"); + + let mut btn_weapons = CreateButton::default(); + btn_weapons + .custom_id(format!("build_weapons_{}_{}", role_index, character_name)) + .style(ButtonStyle::Primary) + .label("Weapons"); + + let mut btn_notes = CreateButton::default(); + btn_notes + .custom_id(format!("build_notes_{}_{}", role_index, character_name)) + .style(ButtonStyle::Primary) + .label("Notes"); + + let mut btn_builds = CreateButton::default(); + btn_builds + .custom_id(format!("build_{}", character_name)) + .style(ButtonStyle::Success) + .label("Other builds"); + + ar.add_button(btn_home); + ar.add_button(btn_artifacts); + ar.add_button(btn_weapons); + ar.add_button(btn_notes); + ar.add_button(btn_builds); + + ar +} + +async fn build_select(ctx: &Context, command: &MessageComponentInteraction, character: String) { + let char = Character::get(character.as_str()).await; + + let roles = &char.builds; + + let roles = roles.into_iter().map(|c| c.name.to_string()).collect::>(); + + let mut rolemap = LinkedHashMap::::new(); + for i in 0..roles.len() { + rolemap.insert(format!("{}_{}", i, character), roles.get(i).expect("").to_string()); + } + + let ar = create_action_row_basic(rolemap, "build_home"); + + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.components(|c| c.add_action_row(ar)) + .create_embed(|e| { + e.title(format!("{}", char.name)) + .thumbnail(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/characters/{}.png", char.id)) + .color(char.element.color) + .description("Select a build !") + .footer(|f| { + f.text(format!("Data from : https://paimon.moe/characters/{}", char.id)) + }) + }) + }) + }).await.expect("Can't send response"); + + return; +} + + +pub async fn home_embed(role: &Role, character: Character) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + embed.title(format!("{} | {}", character.name, role.name)); + embed.thumbnail(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/characters/{}.png", character.id)); + embed.color(character.element.color); + + match role.weapons.get(0) { + Some(t) => { + embed.field("Best Weapon", + Weapon::get(t.id.borrow()).await.name, + true, + ); + } + _ => { + embed.field("Best Weapon", "TBD", true); + } + } + + embed.field("Skill Order", + role.talent.to_owned().into_iter().map(|t| format!("- {}\n", t)) + .collect::>().join("").strip_suffix("\n").expect(""), + true, + ); + + + match role.artifacts.get(0) { + Some(a) => { + let mut artifact_string: Vec = vec![]; + let str; + match a.len() { + 1 => { + let artifact_name = match a.get(0).expect("").to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + + str = format!("(4) {}", artifact_name); + } + 2 => { + for art in a { + let artifact_name = match art.to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + artifact_string.push(format!("(2) {}", artifact_name)); + } + str = artifact_string.join(" & "); + } + _ => { + artifact_string.push("Choose 2 sets :".to_string()); + for art in a { + let artifact_name = match art.to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + artifact_string.push(format!("(2) {}", artifact_name)); + } + str = artifact_string.join(", ") + } + } + embed.field("Best Artifacts", str, false); + } + _ => { embed.field("Best Artifacts", "TBD", false); } + } + + embed.field("Circlet", &role.main_stats.circlet, true); + embed.field("Goblet", &role.main_stats.goblet, true); + embed.field("Sands", &role.main_stats.sands, true); + + embed.footer(|f| { + f.text(format!("Data from : https://paimon.moe/characters/{}", character.id)) + }); + + embed +} + + +async fn artifact_embed(role: &Role, character: Character) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + embed.title(format!("{} | {}", character.name, role.name)); + + match role.artifacts.get(0) { + Some(t) if t.get(0).expect("").to_string() != "TBD" => { + embed.thumbnail(format!("https://raw.githubusercontent.com/MadeBaruna/paimon-moe/main/static/images/artifacts/{}_circlet.png", t.get(0).expect(""))); + } + + _ => { + embed.thumbnail(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/characters/{}.png", character.id)); + } + } + + embed.color(character.element.color); + + let mut all_artefacts_string: Vec = vec![]; + for i in 0..role.artifacts.len() { + match role.artifacts.get(i) { + Some(a) => { + let mut artifact_string: Vec = vec![]; + let str; + match a.len() { + 1 => { + let artifact_name = match a.get(0).expect("").to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + + str = format!("(4) {}", artifact_name); + } + 2 => { + for art in a { + let artifact_name = match art.to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + artifact_string.push(format!("(2) {}", artifact_name)); + } + str = artifact_string.join(" & "); + } + _ => { + artifact_string.push("Choose 2 sets :".to_string()); + for art in a { + let artifact_name = match art.to_string().as_str() { + "+18%_atk_set" => "+18% Atk Set".to_string(), + t => Artifact::get(t).await.name.to_string(), + }; + artifact_string.push(format!("(2) {}", artifact_name)); + } + str = artifact_string.join(", ") + } + } + all_artefacts_string.push(format!("- {}", str)); + } + _ => { all_artefacts_string.push(format!("- TBD")); } + } + } + if all_artefacts_string.len() > 0 { + embed.field("Best Artifacts", all_artefacts_string.join("\n"), false); + } else { + embed.field("Best Artifacts", "- TBD", false); + } + + embed.field("Sub-stats", { + format!("- {}", role.sub_stats.join("\n-")) + }, false); + + embed.field("Circlet", &role.main_stats.circlet, true); + embed.field("Goblet", &role.main_stats.goblet, true); + embed.field("Sands", &role.main_stats.sands, true); + + embed.footer(|f| { + f.text(format!("Data from : https://paimon.moe/characters/{}", character.id)) + }); + + embed +} + +async fn weapons_embed(role: &Role, character: Character) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + embed.title(format!("{} | {}", character.name, role.name)); + + match role.weapons.get(0) { + Some(t) if t.id.to_string() != "TBD" => { + embed.thumbnail(format!("https://raw.githubusercontent.com/MadeBaruna/paimon-moe/main/static/images/weapons/{}.png", t.id)); + } + + _ => { + embed.thumbnail(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/characters/{}.png", character.id)); + } + } + + embed.color(character.element.color); + + let mut all_weapons_string: Vec = vec![]; + for i in 0..role.weapons.len() { + match role.weapons.get(i) { + Some(a) if a.id.to_string() != "TBD" => { + let weapon = Weapon::get(a.id.borrow()).await; + all_weapons_string.push(format!("- {}", weapon.name)); + } + _ => { all_weapons_string.push(format!("- TBD")); } + } + } + if all_weapons_string.len() > 0 { + embed.field("Best Weapons", all_weapons_string.join("\n"), false); + } else { + embed.field("Best Weapons", "- TBD", false); + } + + embed.footer(|f| { + f.text(format!("Data from : https://paimon.moe/characters/{}", character.id)) + }); + + + embed +} + +async fn note_embed(role: &Role, character: Character) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + embed.title(format!("{} | {}", character.name, &role.name)); + embed.thumbnail(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/characters/{}.png", character.id)); + embed.color(character.element.color); + + match &role.note { + n=> { + let x = n.split("\n"); + let mut first = true; + let mut add_before = ""; + for note_paragraph in x.collect::>() { + if note_paragraph.len() == 0 {continue}; + if note_paragraph.len() < 64 && add_before.len() == 0{ + add_before = note_paragraph; + continue; + } + if add_before.len() > 0 { + let note_paragraph = format!("**{}**\n{}", add_before, note_paragraph); + embed.field(if first { "Notes" } else { "\u{200b}" }, note_paragraph, false); + add_before = ""; + } else { + embed.field(if first { "Notes" } else { "\u{200b}" }, note_paragraph, false); + first = false; + } + + } + } + }; + + match &role.tip { + n=> { + let x = n.split("\n"); + let mut first = true; + let mut add_before = ""; + for tip_paragraph in x.collect::>() { + if tip_paragraph.len() == 0 {continue}; + if tip_paragraph.len() < 64 { + add_before = tip_paragraph; + continue; + } + if add_before.len() > 0 { + let tip_paragraph = format!("**{}**\n{}", add_before, tip_paragraph); + embed.field(if first { "Tips" } else { "\u{200b}" }, tip_paragraph, false); + add_before = ""; + } else { + embed.field(if first { "Tips" } else { "\u{200b}" }, tip_paragraph, false); + first = false; + } + + } + } + }; + + + + embed.footer(|f| { + f.text(format!("Data from : https://paimon.moe/characters/{}", character.id)) + }); + + embed +} \ No newline at end of file diff --git a/src/interactions/genshin/mod.rs b/src/interactions/genshin/mod.rs new file mode 100644 index 0000000..174d8b2 --- /dev/null +++ b/src/interactions/genshin/mod.rs @@ -0,0 +1,21 @@ +use serenity::client::Context; +use serenity::model::interactions::application_command::ApplicationCommandInteraction; +use crate::interactions::genshin::artifacts::genshin_artifact_interaction; +use crate::interactions::genshin::build::genshin_build_interaction; +use crate::interactions::genshin::weapons::genshin_weapon_interaction; + +pub mod build; +pub mod weapons; +pub mod artifacts; + + + +pub async fn genshin_interaction(ctx: Context, command: ApplicationCommandInteraction) { + let sub_command = command.data.options.get(0).expect("No command provided"); + match sub_command.name.as_str() { + "builds" => genshin_build_interaction(&ctx, &command, sub_command).await, + "artifact" => genshin_artifact_interaction(&ctx, &command, sub_command).await, + "weapon" => genshin_weapon_interaction(&ctx, &command, sub_command).await, + _ => println!("Unknown Command") + } +} \ No newline at end of file diff --git a/src/interactions/genshin/weapons.rs b/src/interactions/genshin/weapons.rs new file mode 100644 index 0000000..16de0f5 --- /dev/null +++ b/src/interactions/genshin/weapons.rs @@ -0,0 +1,45 @@ +use linked_hash_map::LinkedHashMap; +use serenity::client::Context; +use serenity::model::interactions::application_command::ApplicationCommandInteraction; +use serenity::model::interactions::application_command::ApplicationCommandInteractionDataOption; +use serenity::model::interactions::InteractionResponseType; +use serenity::model::prelude::InteractionApplicationCommandCallbackDataFlags; +use serenity::model::prelude::message_component::MessageComponentInteraction; +use crate::data::weapons::Weapon; +use crate::interactions::utils::create_action_row_basic; + +pub async fn genshin_weapon_interaction(ctx: &Context, command: &ApplicationCommandInteraction, opt: &ApplicationCommandInteractionDataOption) { + + let weapon = opt.options.get(0).expect("No argument for command Genshin build") + .value.as_ref().expect("").as_str().expect("Not a string"); + + let weapons = Weapon::search(weapon).await; + let mut weapon_list: LinkedHashMap = LinkedHashMap::new(); + + for w in weapons { + weapon_list.insert(w.id().to_string(), w.name().to_string()); + } + + let ar = create_action_row_basic(weapon_list, "weapon"); + + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Select a Weapon") + .components(|c| c.add_action_row(ar)) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.expect("The response didn't get sent"); +} + + +pub async fn show_weapon_embed(ctx: &Context, command: &MessageComponentInteraction, weapon_name: String){ + let weapon = Weapon::get(weapon_name.as_str()).await; + command.create_interaction_response(&ctx.http, |res| { + res.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.embeds(vec!(weapon.to_embed()).into_iter()) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.expect("The message didn't got sent"); +} \ No newline at end of file diff --git a/src/interactions/mod.rs b/src/interactions/mod.rs new file mode 100644 index 0000000..44fc3de --- /dev/null +++ b/src/interactions/mod.rs @@ -0,0 +1,30 @@ +pub mod status_message; +pub mod genshin; + +#[path = "../data/mod.rs"] +mod data; +#[path = "../utils/mod.rs"] +pub mod utils; + + + +use serenity::client::Context; +use serenity::model::interactions::{InteractionApplicationCommandCallbackDataFlags, InteractionResponseType}; +use serenity::model::interactions::application_command::ApplicationCommandInteraction; + +pub async fn pong(ctx : Context, command : ApplicationCommandInteraction) { + let res = command.create_interaction_response(ctx.http, |res| { + res.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|response| { + response.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + .content("An error has occurred") + }) + }).await; + + match res { + Ok(()) => {} + Err(e) =>{ + println!("An error has occured : {}", e) + } + } +} \ No newline at end of file diff --git a/src/interactions/status_message.rs b/src/interactions/status_message.rs new file mode 100644 index 0000000..015e47f --- /dev/null +++ b/src/interactions/status_message.rs @@ -0,0 +1,177 @@ +use std::time::Duration; +use rand::Rng; +use serenity::{ + client::{ + Context + } +}; +use serenity::builder::CreateEmbed; +use serenity::model::id::{ChannelId, MessageId}; + +use serenity::model::interactions::application_command::ApplicationCommandInteraction; +use serenity::model::interactions::InteractionApplicationCommandCallbackDataFlags; +use serenity::model::prelude::{InteractionResponseType}; +use serenity::utils::Colour; +use crate::interactions::data::events::Event; +use crate::utils::mongo::{add_discord_status_message, get_all_status_messages, get_discord_status_message, StatusMessage}; + + +pub fn copy_embed(from: &Vec) -> Vec { + let mut cln: Vec = vec![]; + for x in from { + cln.push(x.clone()); + } + cln +} + +pub async fn update_status_message(ctx: Context) { + let forever = tokio::task::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(60*60)); + loop { + let mut x = get_all_status_messages().await; + x.reverse(); + + let embeds = create_status_embed().await; + for sm in x { + let msg = ChannelId::from(sm.channel_id as u64).message(&ctx.http, sm.message_id as u64).await; + match msg { + Ok(mut m) => { + let copies = copy_embed(&embeds); + m.edit(&ctx.http, |f| { + f.set_embeds(copies) + }).await.unwrap(); + + } + Err(_) => {} + } + } + interval.tick().await; + } + }); + forever.await.expect("Stopped for some reasons"); +} + +#[allow(dead_code)] +pub async fn create_status_interaction(ctx: Context, command: ApplicationCommandInteraction) { + let embeds = create_status_embed().await; + let channel_option = &command.data.resolved.channels; + let channel_id = channel_option.keys().next().expect("No options passed"); + + let message = get_discord_status_message(&command.guild_id.expect("Not in guild").as_u64().to_owned()).await; + match message { + Some(e) => { + let rm = ChannelId::from(e.channel_id as u64) + .message(&ctx.http, MessageId::from(e.message_id as u64)) + .await; + match rm { + msg if rm.is_ok() => { + msg.unwrap().delete(&ctx.http).await.unwrap(); + } + _ => {} + } + } + _ => {} + }; + + let msg = channel_id.send_message(&ctx.http, |f| { + f.add_embeds(embeds) + }).await.expect("Can't send Message"); + + let sm = StatusMessage { + message_id: *msg.id.as_u64() as i64, + channel_id: *msg.channel_id.as_u64() as i64, + guild_id: *command.guild_id.expect("Not in guild").as_u64() as i64, + }; + + add_discord_status_message(sm).await; + + + command.create_interaction_response(&ctx.http, |r| { + r.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.create_embed(|e| { + e.title("Command Successful !"); + e.color(Colour::new(0x00ff00)); + e.description(format!("Status message created !")) + }) + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }).await.unwrap(); +} + +async fn create_status_embed() -> Vec { + let mut embeds: Vec = vec![]; + + let mut current = Event::get_current().await; + let mut others = current.clone(); + + let mut question_marks = format!(""); + + let mut upcoming = Event::get_upcoming().await; + + current = current.into_iter().filter(|p| p.show_on_home.unwrap_or(false)).collect::>(); + others = others.into_iter().filter(|p| !p.show_on_home.unwrap_or(false)).collect::>(); + + + upcoming = upcoming.into_iter().filter(|p| p.show_on_home.unwrap_or(false)).collect::>(); + let upcoming_event: Option<&Event> = upcoming.get(0); + + for e in current { + question_marks = format!("{}?", question_marks); + let mut embed = CreateEmbed::default(); + embed.title(e.name); + embed.color(Colour::new(rand::thread_rng().gen_range(0x000000..0xffffff))); + + match &e.image { + Some(url) => { + embed.image(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/events/{}", url)); + } + _ => {} + }; + + match e.url { + Some(t) => { embed.url(format!("{}{}", t, question_marks)); } + _ => {} + }; + + embed.description(format!("Ends : ", e.end_timestamp)); + embeds.push(embed); + } + + +// Other events embed + let mut other_embed = CreateEmbed::default(); + other_embed.title("Other Events"); + other_embed.color(Colour::new(rand::thread_rng().gen_range(0x000000..0xffffff))); + + for e in others { + other_embed.field(e.name, format!("Ends : ", e.end_timestamp), false); + } + embeds.push(other_embed); + + match upcoming_event { + Some(e) => { + let mut upcoming_embed = CreateEmbed::default(); + question_marks = format!("{}?", question_marks); + upcoming_embed.title(&e.name); + upcoming_embed.description(format!("Starts : ", e.start_timestamp)); + upcoming_embed.color(Colour::new(rand::thread_rng().gen_range(0x000000..0xffffff))); + + match &e.image { + Some(url) => { + upcoming_embed.image(format!("https://github.com/MadeBaruna/paimon-moe/raw/main/static/images/events/{}", url)); + } + _ => {} + }; + + match &e.url { + Some(url) => { upcoming_embed.url(format!("{}{}", url, question_marks)); } + _ => {} + }; + embeds.push(upcoming_embed); + } + _ => {} + }; + + embeds +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ecd6ffa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,164 @@ +mod interactions; +mod data; +pub mod utils; + +use std::env; +use dotenv::dotenv; +use serenity::{ + async_trait, + model::{ + gateway::Ready, + interactions::{ + application_command::{ + ApplicationCommand, + ApplicationCommandOptionType, + }, + Interaction, + }, + }, + prelude::*, +}; +use serenity::client::bridge::gateway::GatewayIntents; +use serenity::model::gateway::Activity; +use crate::interactions::genshin::artifacts::show_artifact_embed; +use crate::interactions::genshin::build::build_interact; +use crate::interactions::genshin::weapons::show_weapon_embed; +use crate::interactions::status_message::update_status_message; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _ctx: Context, _bot: Ready) { + println!("{} connected!", _bot.user.name); + update_status_message(_ctx.clone()).await; + _ctx.set_activity(Activity::playing("Drinking")).await; + + let x = ApplicationCommand::get_global_application_commands(&_ctx.http).await.unwrap(); + for i in x { + match i.name.as_str() { + "genshin" => (), + "createstatusmessage" => (), + _ => { + let _result_delete = ApplicationCommand::delete_global_application_command(&_ctx.http, i.id).await; + match _result_delete { + Ok(()) => { println!("Deleted command {}", i.name) } + Err(f) => { println!("An error occurred deleting {} : {}", i.name, f) } + } + } + } + } + + let x = ApplicationCommand::create_global_application_command(&_ctx.http, |command| { + command.name("genshin").description("Get Informations about Genshin Impact.").create_option(|option| { + option.name("builds") + .description("Shows builds for a Genshin Impact Characters") + .kind(ApplicationCommandOptionType::SubCommand) + .create_sub_option(|so| { + so.name("character").description("Character to get builds for") + .kind(ApplicationCommandOptionType::String) + .required(true) + }) + }) + .create_option(|option| { + option.name("weapon") + .description("Shows infos on a Genshin Impact weapon.") + .kind(ApplicationCommandOptionType::SubCommand) + .create_sub_option(|so| { + so.name("name").description("Weapon you want infos on.") + .kind(ApplicationCommandOptionType::String) + .required(true) + }) + }) + .create_option(|option| { + option.name("artifact") + .description("Shows infos on a Genshin Impact artifact set.") + .kind(ApplicationCommandOptionType::SubCommand) + .create_sub_option(|so| { + so.name("artifact") + .description("Artifact Set you want infos on.") + .kind(ApplicationCommandOptionType::String) + .required(true) + }) + }) + }).await; + if x.is_err() { + println!("{}", x.unwrap_err()); + } + + ApplicationCommand::create_global_application_command(&_ctx.http, |command| { + command.name("createstatusmessage").create_option(|opt| { + opt.name("channel").description("Channel where to put the status message") + .kind(ApplicationCommandOptionType::Channel) + .required(true) + }).description("Creates a status message of all the current events on Genshin Impact") + }).await.expect("Can't create the createstatusmessage command"); + } + + + async fn interaction_create(&self, _ctx: Context, _interaction: Interaction) { + if let Interaction::ApplicationCommand(command) = _interaction { + match command.data.name.as_str() { + // Ping command + "genshin" => interactions::genshin::genshin_interaction(_ctx, command).await, + "createstatusmessage" => interactions::status_message::create_status_interaction(_ctx, command).await, + // Unknown command + _ => interactions::pong(_ctx, command).await + } + } else if let Interaction::MessageComponent(component) = _interaction { + let mut args = component.data.custom_id.split("_"); + let command = args.next(); + match command.unwrap() { + "weapon" => { + let weapon = args.collect::>().join("_"); + show_weapon_embed(&_ctx, &component, weapon).await; + } + "artifact" => { + let artifact = args.collect::>().join("_"); + show_artifact_embed(&_ctx, &component, artifact).await; + } + "build" => { + let character = args.collect::>().join("_"); + build_interact(&_ctx, &component, character).await; + } + _ => println!("Unknown interaction") + } + } else { + println!("ERROR"); + } + } +} + + +#[tokio::main] +async fn main() { + dotenv().ok(); + let mut token= "".to_string(); + for (k, v) in env::vars() { + if k.eq("DISCORD_TOKEN") { + token = v; + break; + } + } + + let application_id: u64 = "860553396578811914".parse().expect("Wrong format"); + + let needed_intents = [ + GatewayIntents::GUILDS, + GatewayIntents::GUILD_MESSAGES, + GatewayIntents::GUILD_EMOJIS, + GatewayIntents::GUILD_WEBHOOKS, + GatewayIntents::GUILD_INTEGRATIONS + ]; + + let mut client = Client::builder(token) + .event_handler(Handler) + .intents(GatewayIntents::from_iter(needed_intents.into_iter())) + .application_id(application_id) + .await + .expect("Error creating the client"); + + if let Err(why) = client.start().await { + println!("Client error {}", why); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..3454acf --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,22 @@ +use linked_hash_map::LinkedHashMap; +use serenity::builder::{CreateActionRow, CreateButton}; +use serenity::model::interactions::message_component::ButtonStyle; + +pub mod mongo; + +pub fn create_action_row_basic (dict: LinkedHashMap, command: &str) -> CreateActionRow { + let mut buttons: Vec = vec!(); + for d in dict { + let mut b = CreateButton::default(); + b.custom_id(format!("{}_{}", command, &d.0)); + b.label(d.1); + b.style(ButtonStyle::Primary); + buttons.push(b) + } + + let mut ar = CreateActionRow::default(); + for btn in buttons { + ar.add_button(btn); + } + ar +} diff --git a/src/utils/mongo.rs b/src/utils/mongo.rs new file mode 100644 index 0000000..679a0b8 --- /dev/null +++ b/src/utils/mongo.rs @@ -0,0 +1,67 @@ +use mongodb::bson::{Bson, doc, Document, from_bson, to_bson}; +use mongodb::{Client, Collection, options::ClientOptions}; +use futures::stream::{TryStreamExt}; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct StatusMessage { + pub(crate) guild_id: i64, + pub(crate) message_id: i64, + pub(crate) channel_id: i64 +} + +#[allow(dead_code)] +pub async fn add_discord_status_message(status_message: StatusMessage) -> bool { + let client = get_mongo_client().await; + + let serialized = to_bson(&status_message).expect("Can't serialize status_message"); + let document = serialized.as_document().unwrap(); + + let collection: Collection = client.database("drunk_venti").collection("StatusMessages"); + let updated = collection.update_one(doc! {"guild_id": status_message.guild_id}, doc! {"$set" : document}, None).await; + let is_ok = &updated.is_ok(); + match updated.unwrap() { + e if e.matched_count == 0 => { + let inserted = collection.insert_one(document, None).await; + inserted.is_ok() + } + _ => *is_ok + } +} + +#[allow(dead_code)] +pub async fn get_discord_status_message(gid: &u64) -> Option { + let client = get_mongo_client().await; + + let status_messages: Collection = client.database("drunk_venti").collection("StatusMessages"); + + let infos = status_messages.find_one(doc! {"guild_id" : *gid as i64}, None).await.expect("Can't find one"); + + + match infos { + Some(i) => { + let m_infos: StatusMessage = from_bson(Bson::Document(i)).expect("Can't get"); + return Some(m_infos); + } + _ => None + } +} + +#[allow(dead_code)] +pub async fn get_all_status_messages() -> Vec { + let client = get_mongo_client().await; + + let collection: Collection = client.database("drunk_venti").collection::("StatusMessages"); + let documents = collection.find(None, None).await.expect("Can't get everything"); + let all_docs: Vec = documents.try_collect().await.unwrap_or_else(|_| vec![]); + return all_docs; +} + +#[allow(dead_code)] +async fn get_mongo_client() -> Client { + let mut client_options = ClientOptions::parse("mongodb://localhost:27017/?readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false").await.expect("Can't connect to db"); + client_options.app_name = Some("Drunk Venti".to_string()); + client_options.default_database = Some("drunk_venti".to_string()); + + Client::with_options(client_options).expect("Can't add options") +} \ No newline at end of file