diff --git a/.idea/patched-porobot.iml b/.idea/patched-porobot.iml index 20bba95..121e142 100644 --- a/.idea/patched-porobot.iml +++ b/.idea/patched-porobot.iml @@ -7,6 +7,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index da36158..4d4017f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1036,8 +1036,10 @@ version = "0.3.0" dependencies = [ "glob", "itertools 0.10.3", + "lazy_static", "log", "pretty_env_logger", + "regex", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 0233602..77d9fa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ categories = ["games", "parser-implementations"] [dependencies] # base log = { version = "0.4.17" } -itertools = { version = "0.10.3" } # Not using this yet +itertools = { version = "0.10.3" } +regex = { version = "1.6.0" } +lazy_static = { version = "1.4.0" } # exec pretty_env_logger = { version = "0.4.0", optional = true } glob = { version = "0.3.0", optional = true } diff --git a/src/bin/patched_porobot_telegram.rs b/src/bin/patched_porobot_telegram.rs new file mode 100644 index 0000000..08ea74d --- /dev/null +++ b/src/bin/patched_porobot_telegram.rs @@ -0,0 +1,127 @@ +#[cfg(not(feature = "telegram"))] +fn main() { + println!("The `telegram` feature was not included on compilation, therefore this binary is not available.") +} + +#[cfg(feature = "telegram")] +#[tokio::main] +async fn main() { + use std::path::PathBuf; + use log::*; + use patched_porobot::data::setbundle::card::{Card, CardIndex}; + use patched_porobot::data::corebundle::CoreBundle; + use patched_porobot::data::setbundle::SetBundle; + use patched_porobot::data::corebundle::globals::LocalizedGlobalsIndexes; + use patched_porobot::search::cardsearch::CardSearchEngine; + use patched_porobot::telegram::inline::card_to_inlinequeryresult; + use teloxide::payloads::AnswerInlineQuery; + use teloxide::requests::JsonRequest; + use teloxide::prelude::*; + use itertools::Itertools; + + pretty_env_logger::init(); + debug!("Logger initialized successfully!"); + + debug!("Loading bundles..."); + let core = CoreBundle::load(&*PathBuf::from("./card-data/core-en_us")).expect("to be able to load `core-en_us` bundle"); + let set1 = SetBundle::load(&*PathBuf::from("./card-data/set1-en_us")).expect("to be able to load `set1-en_us` bundle"); + let set2 = SetBundle::load(&*PathBuf::from("./card-data/set2-en_us")).expect("to be able to load `set2-en_us` bundle"); + let set3 = SetBundle::load(&*PathBuf::from("./card-data/set3-en_us")).expect("to be able to load `set3-en_us` bundle"); + let set4 = SetBundle::load(&*PathBuf::from("./card-data/set4-en_us")).expect("to be able to load `set4-en_us` bundle"); + let set5 = SetBundle::load(&*PathBuf::from("./card-data/set5-en_us")).expect("to be able to load `set5-en_us` bundle"); + let set6 = SetBundle::load(&*PathBuf::from("./card-data/set6-en_us")).expect("to be able to load `set6-en_us` bundle"); + debug!("Loaded all bundles!"); + + debug!("Indexing globals..."); + let globals = LocalizedGlobalsIndexes::from(core.globals); + debug!("Indexed globals!"); + + debug!("Indexing cards..."); + let cards: Vec = [ + set1.cards, + set2.cards, + set3.cards, + set4.cards, + set5.cards, + set6.cards + ].concat(); + + let mut index = CardIndex::new(); + for card in cards { + index.insert(card.code.clone(), card); + } + let cards = index; + debug!("Indexed cards!"); + + debug!("Creating search engine..."); + let engine = CardSearchEngine::new(globals, cards); + debug!("Created search engine!"); + + debug!("Creating Telegram bot with parameters from the environment..."); + let bot = Bot::from_env(); + let me = bot.get_me().send().await.expect("Telegram bot parameters to be valid"); + debug!("Created Telegram bot!"); + + debug!("Creating inline query handler..."); + let handler = Update::filter_inline_query().chain(dptree::endpoint(move |query: InlineQuery, bot: Bot| { + info!("Handling inline query: `{}`", &query.query); + + debug!("Querying the search engine..."); + let performed_query = engine.query(&query.query, 50); + + let payload = match performed_query { + Ok(results) => { + if results.len() > 0 { + AnswerInlineQuery { + inline_query_id: query.id.clone(), + results: results + .iter() + .map(|card| card_to_inlinequeryresult(&engine.globals, card)) + .collect_vec(), + cache_time: Some(86400), + is_personal: Some(false), + next_offset: None, + switch_pm_text: None, + switch_pm_parameter: None, + } + } + else { + AnswerInlineQuery { + inline_query_id: query.id.clone(), + results: vec![], + cache_time: None, + is_personal: Some(false), + next_offset: None, + switch_pm_text: Some("No results found".to_string()), + switch_pm_parameter: Some("err-no-results".to_string()), + } + } + } + Err(_) => { + AnswerInlineQuery { + inline_query_id: query.id.clone(), + results: vec![], + cache_time: None, + is_personal: Some(false), + next_offset: None, + switch_pm_text: Some("Invalid query syntax".to_string()), + switch_pm_parameter: Some("err-invalid-query".to_string()), + } + } + }; + + async move { + let telegram_reply = JsonRequest::new(bot.clone(), payload).send().await; + + if let Err(e) = telegram_reply { + error!("{:?}", &e); + } + + respond(()) + } + })); + debug!("Create inline query handler!"); + + info!("@{} is ready!", &me.username.as_ref().expect("bot to have an username")); + Dispatcher::builder(bot, handler).enable_ctrlc_handler().build().dispatch().await; +} diff --git a/src/bin/telegrambot.rs b/src/bin/telegrambot.rs deleted file mode 100644 index b9ccf66..0000000 --- a/src/bin/telegrambot.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(not(feature = "telegram"))] -fn main() { - println!("The `telegram` feature was not included on compilation, therefore this binary is not available.") -} - -#[cfg(feature = "telegram")] -fn main() { - println!("Hello telegram world!") -} diff --git a/src/data/setbundle/art.rs b/src/data/setbundle/art.rs index e307ae7..2aa8d22 100644 --- a/src/data/setbundle/art.rs +++ b/src/data/setbundle/art.rs @@ -1,6 +1,9 @@ //! Module defining [CardArt]. +use lazy_static::lazy_static; +use regex::Regex; + /// The illustration of a [Card](super::card::Card), also referred to as an *art asset*. #[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct CardArt { @@ -40,9 +43,13 @@ impl CardArt { /// ``` /// pub fn card_jpg(&self) -> String { - self.card_png - .replace("https://dd.b.pvp.net/latest/set1", "https://poro.steffo.eu/set1-en_us") - .replace(".png", ".jpg") + lazy_static! { + static ref GET_JPG: Regex = Regex::new( + r#"https?://dd[.]b[.]pvp[.]net/[^/]+/(?P[^/]+)/(?P[^/]+)/img/cards/(?P.+)[.]png$"# + ).unwrap(); + } + + GET_JPG.replace_all(&self.card_png, "https://poro.steffo.eu/$bundle-$locale/$locale/img/cards/$code.jpg").to_string() } /// URL to the `.jpg` image of the `en_us` locale of the full card art, via `poro.steffo.eu`. @@ -56,9 +63,13 @@ impl CardArt { /// ``` /// pub fn full_jpg(&self) -> String { - self.full_png - .replace("https://dd.b.pvp.net/latest/set1", "https://poro.steffo.eu/set1-en_us") - .replace(".png", ".jpg") + lazy_static! { + static ref GET_JPG: Regex = Regex::new( + r#"https?://dd[.]b[.]pvp[.]net/[^/]+/(?P[^/]+)/(?P[^/]+)/img/cards/(?P.+)[.]png$"# + ).unwrap(); + } + + GET_JPG.replace_all(&self.full_png, "https://poro.steffo.eu/$bundle-$locale/$locale/img/cards/$code.jpg").to_string() } } diff --git a/src/search/cardsearch.rs b/src/search/cardsearch.rs index 54984eb..66c1c43 100644 --- a/src/search/cardsearch.rs +++ b/src/search/cardsearch.rs @@ -47,12 +47,17 @@ impl CardSearchEngine { /// Create the [tantivy::schema::TextOptions] for card codes. /// /// Card codes should: - /// - never be tokenized; + /// - TODO: be tokenized without alterations; + /// - ignore positioning; /// - be retrievable (what [tantivy] calls "stored"). fn options_code() -> TextOptions { use tantivy::schema::*; TextOptions::default() + .set_indexing_options(TextFieldIndexing::default() + .set_tokenizer("card") + .set_index_option(IndexRecordOption::Basic) + ) .set_stored() .set_fast() } @@ -253,7 +258,14 @@ impl CardSearchEngine { fn parser(index: &Index, fields: CardSchemaFields) -> QueryParser { QueryParser::for_index( &index, - Vec::from(fields) + vec![ + fields.code, + fields.name, + fields.description, + fields.flavor, + fields.subtypes, + fields.supertype, + ] ) } @@ -346,28 +358,3 @@ struct CardSchemaFields { /// [Card::supertype]. pub supertype: Field, } - -impl From for Vec { - fn from(fields: CardSchemaFields) -> Self { - vec![ - fields.code, - fields.name, - fields.r#type, - fields.set, - fields.rarity, - fields.collectible, - fields.regions, - fields.attack, - fields.cost, - fields.health, - fields.spellspeed, - fields.keywords, - fields.description, - fields.levelup, - fields.flavor, - fields.artist, - fields.subtypes, - fields.supertype, - ] - } -} diff --git a/src/telegram/display.rs b/src/telegram/display.rs index 9fb1c07..b40c85e 100644 --- a/src/telegram/display.rs +++ b/src/telegram/display.rs @@ -22,24 +22,23 @@ use crate::data::setbundle::supertype::CardSupertype; /// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style pub fn display_card(globals: &LocalizedGlobalsIndexes, card: &Card) -> String { let title = format!( - r#"{}"#, - &card.main_art().expect("Card to have at least one illustration").card_png, + "{}\n", escape(&card.name), ); let stats = match &card.r#type { CardType::Spell => format!( - "{} mana", + "{} mana\n", escape(&card.cost.to_string()), ), CardType::Unit => format!( - "{} mana {}|{}", + "{} mana {}|{}\n", escape(&card.cost.to_string()), escape(&card.attack.to_string()), escape(&card.health.to_string()), ), CardType::Landmark => format!( - "{} mana", + "{} mana\n", &card.cost ), _ => "".to_string(), @@ -49,32 +48,25 @@ pub fn display_card(globals: &LocalizedGlobalsIndexes, card: &Card) -> String { let regions = display_regions(&card.regions, &globals.regions); let r#type = display_types(&card.r#type, &card.supertype, &card.subtypes); - let breadcrumbs = format!("{} › {} › {}", &set, ®ions, &r#type); + let breadcrumbs = format!("{} › {} › {}\n", &set, ®ions, &r#type); let keywords = display_keywords(&card.keywords, &globals.keywords); - let description = escape(&card.localized_description_text); + let description = format!("{}\n", escape(&card.localized_description_text)); let flavor = format!( - "{}", + "{}\n", escape(&card.localized_flavor_text) ); let artist = format!( - r#"Illustration by {}"#, + r#"Illustration by {}"#, &card.main_art().expect("Card to have at least one illustration").full_png, escape(&card.artist_name) ); format!( - "{title} {stats}\n{breadcrumbs}\n\n{keywords}\n{description}\n\n-----\n{flavor}\n\n{artist}", - title=title, - stats=stats, - breadcrumbs=breadcrumbs, - keywords=keywords, - description=description, - flavor=flavor, - artist=artist, + "{title}{breadcrumbs}\n{keywords}{stats}{description}\n-----\n{flavor}{artist}", ) } @@ -145,12 +137,16 @@ fn display_types(r#type: &CardType, supertype: &CardSupertype, subtypes: &[CardS /// /// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style fn display_keywords(keywords: &[CardKeyword], hm: &LocalizedCardKeywordIndex) -> String { - keywords - .iter() - .map(|keyword| keyword - .localized(hm) - .map(|o| format!("[{}]", escape(&o.name))) - .unwrap_or_else(|| "Unknown".to_string()) - ) - .join(" ") + format!( + "{}\n", + keywords + .iter() + .map(|keyword| keyword + .localized(hm) + .map(|o| format!("[{}]", escape(&o.name))) + .unwrap_or_else(|| "Unknown".to_string()) + ) + .join(" ") + ) + } diff --git a/src/telegram/inline.rs b/src/telegram/inline.rs index 168a810..7b72f8f 100644 --- a/src/telegram/inline.rs +++ b/src/telegram/inline.rs @@ -2,7 +2,7 @@ //! //! [inline mode]: https://core.telegram.org/bots/api#inline-mode -use teloxide::types::{InlineQueryResult, InlineQueryResultPhoto, InputMessageContent, InputMessageContentText, ParseMode}; +use teloxide::types::{InlineQueryResult, InlineQueryResultPhoto, ParseMode}; use crate::data::corebundle::globals::LocalizedGlobalsIndexes; use crate::data::setbundle::card::Card; use crate::telegram::display::display_card; @@ -13,6 +13,8 @@ pub fn card_to_inlinequeryresult(globals: &LocalizedGlobalsIndexes, card: &Card) InlineQueryResult::Photo(InlineQueryResultPhoto { id: card.code.to_owned(), title: Some(card.name.to_owned()), + caption: Some(display_card(&globals, &card)), + parse_mode: Some(ParseMode::Html), photo_url: card .main_art() .expect("Card to have at least one illustration") @@ -26,17 +28,9 @@ pub fn card_to_inlinequeryresult(globals: &LocalizedGlobalsIndexes, card: &Card) photo_width: Some(680), photo_height: Some(1024), - input_message_content: Some(InputMessageContent::Text(InputMessageContentText { - message_text: display_card(&globals, &card), - parse_mode: Some(ParseMode::Html), - entities: None, - disable_web_page_preview: Some(true) - })), - description: None, - caption: None, - parse_mode: None, caption_entities: None, reply_markup: None, + input_message_content: None, }) }