diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..be20421 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: "Continuous integration" + +on: + pull_request: + branches: + - main + workflow_call: + + +jobs: + cargotest: + steps: + - name: "Checkout repository" + uses: actions/checkout@v3 + + - name: "Install Rust toolchain" + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: "Run cargo clippy" + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: "Run cargo test" + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/Cargo.lock b/Cargo.lock index 819190f..e00c07e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,7 +1039,7 @@ dependencies = [ [[package]] name = "patched_porobot" -version = "0.6.0" +version = "0.7.0" dependencies = [ "data-encoding", "glob", diff --git a/Cargo.toml b/Cargo.toml index 61d7835..10e1e29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "patched_porobot" -version = "0.6.0" +version = "0.7.0" authors = ["Stefano Pigozzi "] edition = "2021" description = "Legends of Runeterra card database utilities and bots" diff --git a/Dockerfile b/Dockerfile index 85a4ee0..21ae69d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.62 AS labels +FROM rust:1.64 AS labels LABEL org.opencontainers.image.title="Patched Porobot" LABEL org.opencontainers.image.description="Legends of Runeterra card database utilities and bots" diff --git a/src/bin/patched_porobot_telegram.rs b/src/bin/patched_porobot_telegram.rs index 7abdd6a..7157406 100644 --- a/src/bin/patched_porobot_telegram.rs +++ b/src/bin/patched_porobot_telegram.rs @@ -72,7 +72,7 @@ //! //! Since [@patchedporobot] uses [`tantivy`] internally, you might find more information on even more advanced queries in the [documentation of their `QueryParser`](tantivy::query::QueryParser)! //! -//! ### Deck queries +//! ### Deck parsing //! //! You can have [@patchedporobot] display a deck and its cards by pasting the deck code after the bot's username: //! @@ -82,6 +82,15 @@ //! //! Then, select the "Deck with N cards" option to send the deck's card list in the chat! //! +//! #### Named decks +//! +//! Optionally, you may add a name to your deck, which will be displayed above the deck code: +//! +//! ```text +//! @patchedporobot CIBQCAICAQAQGBQIBEBAMBAJBMGBUHJNGE4AEAIBAIYQEAQGEU2QCAIBAIUQ Gimbo's Depths +//! ``` +//! +//! If entered correctly, the bot will display a slightly different option containing the deck's name (_Deck "NAME" with N cards_), which you can check before the message is sent to the chat. //! //! [@patchedporobot]: https://t.me/patchedporobot diff --git a/src/data/deckcode/deck.rs b/src/data/deckcode/deck.rs index 34f56aa..1a2ac12 100644 --- a/src/data/deckcode/deck.rs +++ b/src/data/deckcode/deck.rs @@ -198,9 +198,7 @@ impl Deck { let card_count = reader.read_u32_varint().map_err(DeckDecodingError::Read)?; let set = reader.read_u32_varint().map_err(DeckDecodingError::Read)?; - let set = CardSet::from(set) - .to_code() - .ok_or(DeckDecodingError::UnknownSet)?; + let set = format!("{:02}", &set); let region = reader.read_u32_varint().map_err(DeckDecodingError::Read)?; let region = CardRegion::from(region) @@ -229,8 +227,7 @@ impl Deck { .write_u32_varint(len) .map_err(DeckEncodingError::Write)?; - let set: u32 = CardSet::from_code(set) - .try_into() + let set: u32 = set.parse() .map_err(|_| DeckEncodingError::UnknownSet)?; writer .write_u32_varint(set) @@ -484,9 +481,9 @@ pub type DeckEncodingResult = Result; #[macro_export] macro_rules! deck { [$($cd:literal: $qty:literal),* $(,)?] => { - crate::data::deckcode::deck::Deck { + $crate::data::deckcode::deck::Deck { contents: std::collections::HashMap::from([ - $((crate::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)* + $(($crate::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)* ]) } } diff --git a/src/data/setbundle/set.rs b/src/data/setbundle/set.rs index 637cfdb..5962e88 100644 --- a/src/data/setbundle/set.rs +++ b/src/data/setbundle/set.rs @@ -55,8 +55,10 @@ impl CardSet { hm.get(self) } - /// Get the [`CardSet`] from its short code, **assuming it is not an [`CardSet::Events`] card**. + /// Get the [`CardSet`] from its short code. /// + /// [`CardSet::Worldwalker`] and [`CardSet::TheDarkinSaga`] share the same code `06`, so a variant cannot be determined. + /// /// [`CardSet::Events`] cards have the short code of the set they were released in, so it is impossible to determine if a card belongs to that set from its short code. pub fn from_code(value: &str) -> Self { match value { @@ -65,7 +67,6 @@ impl CardSet { "03" => Self::CallOfTheMountain, "04" => Self::EmpiresOfTheAscended, "05" => Self::BeyondTheBandlewood, - "06" => Self::Worldwalker, _ => Self::Unsupported, } @@ -84,6 +85,7 @@ impl CardSet { Self::EmpiresOfTheAscended => Some("04".to_string()), Self::BeyondTheBandlewood => Some("05".to_string()), Self::Worldwalker => Some("06".to_string()), + Self::TheDarkinSaga => Some("06".to_string()), _ => None, } @@ -92,6 +94,8 @@ impl CardSet { /// Get the [`CardSet`] from its internal id. /// +/// [`CardSet::Worldwalker`] and [`CardSet::TheDarkinSaga`] share the same id, so a variant cannot be determined. +/// /// [`CardSet::Events`] cards have the id of the set they were released in, so it is impossible to determine if a card belongs to that set from its id. impl From for CardSet { fn from(value: u32) -> Self { @@ -101,7 +105,6 @@ impl From for CardSet { 3 => CardSet::CallOfTheMountain, 4 => CardSet::EmpiresOfTheAscended, 5 => CardSet::BeyondTheBandlewood, - 6 => CardSet::Worldwalker, _ => CardSet::Unsupported, } } @@ -121,6 +124,7 @@ impl TryFrom for u32 { CardSet::EmpiresOfTheAscended => Ok(4), CardSet::BeyondTheBandlewood => Ok(5), CardSet::Worldwalker => Ok(6), + CardSet::TheDarkinSaga => Ok(6), _ => Err(()), } } diff --git a/src/telegram/display.rs b/src/telegram/display.rs index d53170a..03645be 100644 --- a/src/telegram/display.rs +++ b/src/telegram/display.rs @@ -153,10 +153,10 @@ fn display_levelup(levelup: &String) -> String { } } -/// Render a [Deck] in [Telegram Bot HTML]. +/// Render a [Deck] in [Telegram Bot HTML], with an optional `name`. /// /// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style -pub fn display_deck(index: &CardIndex, deck: &Deck, code: String) -> String { +pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&str>) -> String { // TODO: optimize this let cards = deck .contents @@ -182,5 +182,8 @@ pub fn display_deck(index: &CardIndex, deck: &Deck, code: String) -> String { }) .join("\n"); - format!("{}\n\n{}", &code, &cards) + match name { + Some(name) => format!("{}\n{}\n\n{}", &name, &code, &cards), + None => format!("{}\n\n{}", &code, &cards), + } } diff --git a/src/telegram/handler.rs b/src/telegram/handler.rs index c2fff99..7cc1f34 100644 --- a/src/telegram/handler.rs +++ b/src/telegram/handler.rs @@ -10,6 +10,8 @@ use teloxide::payloads::{AnswerInlineQuery, SendMessage}; use teloxide::prelude::*; use teloxide::requests::{JsonRequest, ResponseResult}; use teloxide::types::{ParseMode, Recipient}; +use lazy_static::lazy_static; +use regex::Regex; /// Handle inline queries by searching cards on the [CardSearchEngine]. pub fn inline_query_handler( @@ -33,17 +35,28 @@ pub fn inline_query_handler( }; } - if let Ok(deck) = Deck::from_code(&query.query.to_ascii_uppercase()) { - debug!("Parsed deck successfully!"); - break AnswerInlineQuery { - inline_query_id: query.id.clone(), - results: vec![deck_to_inlinequeryresult(&engine.cards, &deck)], - cache_time: None, - is_personal: Some(false), - next_offset: None, - switch_pm_text: None, - switch_pm_parameter: None, - }; + lazy_static! { + static ref DECK_RE: Regex = Regex::new(r#"^(?P[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+)(?:\s+(?P.+?))?\s*$"#).unwrap(); + } + + if let Some(deck_captures) = DECK_RE.captures(&query.query) { + if let Some(deck_code) = deck_captures.name("code") { + if let Ok(deck) = Deck::from_code(&deck_code.as_str()) { + + debug!("Parsed deck successfully!"); + let name = deck_captures.name("name").map(|m| m.as_str()); + + break AnswerInlineQuery { + inline_query_id: query.id.clone(), + results: vec![deck_to_inlinequeryresult(&engine.cards, &deck, &name)], + cache_time: None, + is_personal: Some(false), + next_offset: None, + switch_pm_text: None, + switch_pm_parameter: None, + }; + } + } } debug!("Querying the card search engine..."); diff --git a/src/telegram/inline.rs b/src/telegram/inline.rs index 411e5a9..8eae007 100644 --- a/src/telegram/inline.rs +++ b/src/telegram/inline.rs @@ -7,15 +7,12 @@ use crate::data::deckcode::deck::Deck; use crate::data::deckcode::format::DeckCodeFormat; use crate::data::setbundle::card::{Card, CardIndex}; use crate::telegram::display::{display_card, display_deck}; -use std::collections::hash_map::DefaultHasher; -use std::hash::Hash; -use std::ptr::hash; use teloxide::types::{ InlineQueryResult, InlineQueryResultArticle, InlineQueryResultPhoto, InputMessageContent, InputMessageContentText, ParseMode, }; -/// Converts a [Card] into a [InlineQueryResult]. +/// Convert a [Card] into a [InlineQueryResult]. pub fn card_to_inlinequeryresult( globals: &LocalizedGlobalsIndexes, card: &Card, @@ -46,16 +43,20 @@ pub fn card_to_inlinequeryresult( }) } -pub fn deck_to_inlinequeryresult(index: &CardIndex, deck: &Deck) -> InlineQueryResult { +/// Convert a [Deck] with an optional name into a [InlineQueryResult]. +pub fn deck_to_inlinequeryresult(index: &CardIndex, deck: &Deck, name: &Option<&str>) -> InlineQueryResult { let code = deck .to_code(DeckCodeFormat::F1) .expect("serialized deck to deserialize properly"); InlineQueryResult::Article(InlineQueryResultArticle { id: format!("{:x}", md5::compute(&code)), - title: format!("Deck with {} cards", deck.contents.len()), + title: match &name { + Some(name) => format!(r#"Deck "{}" with {} cards"#, name, deck.contents.len()), + None => format!("Deck with {} cards", deck.contents.len()) + }, input_message_content: InputMessageContent::Text(InputMessageContentText { - message_text: display_deck(index, deck, code), + message_text: display_deck(index, deck, &code, &name), parse_mode: Some(ParseMode::Html), entities: None, disable_web_page_preview: Some(true),