mirror of
https://github.com/Steffo99/patched-porobot.git
synced 2024-12-23 01:54:22 +00:00
Start work on Telegram card searching
This commit is contained in:
parent
3fbdfc68ec
commit
63a14ab2ef
10 changed files with 226 additions and 10 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1032,7 +1032,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "patched-porobot"
|
name = "patched-porobot"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"itertools 0.10.3",
|
"itertools 0.10.3",
|
||||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "patched-porobot"
|
name = "patched-porobot"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Legends of Runeterra card database utilities and bots"
|
description = "Legends of Runeterra card database utilities and bots"
|
||||||
|
@ -34,3 +34,11 @@ search = ["tantivy"]
|
||||||
telegram = ["search", "teloxide", "reqwest", "tokio"]
|
telegram = ["search", "teloxide", "reqwest", "tokio"]
|
||||||
# discord = ["search"]
|
# discord = ["search"]
|
||||||
# matrix = ["search"]
|
# matrix = ["search"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "patched_porobot"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "patched_porobot_telegram"
|
||||||
|
path = "src/telegram/bin.rs"
|
||||||
|
|
|
@ -3,3 +3,6 @@ pub mod load;
|
||||||
|
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|
||||||
|
#[cfg(feature = "telegram")]
|
||||||
|
pub mod telegram;
|
||||||
|
|
30
src/search/botindex.rs
Normal file
30
src/search/botindex.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
//! This module provides functions to manage [tantivy] [Index]es for internal bot usage.
|
||||||
|
|
||||||
|
use tantivy::{Index, IndexReader, IndexWriter, LeasedItem, ReloadPolicy, Searcher};
|
||||||
|
use crate::search::card::card_schema;
|
||||||
|
|
||||||
|
|
||||||
|
/// Build a new [Index] for [crate::schena::setbundle::Card] documents, based on [card_schema].
|
||||||
|
pub fn card_index() -> Index {
|
||||||
|
Index::create_in_ram(
|
||||||
|
card_schema()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Build a [IndexWriter] with the optimal configuration for [crate::schena::setbundle::Card] documents.
|
||||||
|
pub fn card_writer(index: &Index) -> IndexWriter {
|
||||||
|
index
|
||||||
|
.writer(4_000_000)
|
||||||
|
.expect("to be able to allocate 4 MB for a IndexWriter")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Build a [IndexReader] with the optimal configuration for [crate::schena::setbundle::Card] documents.
|
||||||
|
pub fn card_reader(index: &Index) -> IndexReader {
|
||||||
|
index
|
||||||
|
.reader_builder()
|
||||||
|
.reload_policy(ReloadPolicy::Manual)
|
||||||
|
.try_into()
|
||||||
|
.expect("to be able to create a IndexReader")
|
||||||
|
}
|
86
src/search/botsearch.rs
Normal file
86
src/search/botsearch.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
//! This module provides functions to perform queries for internal bot usage.
|
||||||
|
|
||||||
|
|
||||||
|
use tantivy::{Index, IndexReader, TantivyError};
|
||||||
|
use tantivy::collector::TopDocs;
|
||||||
|
use tantivy::query::{QueryParser, QueryParserError};
|
||||||
|
use tantivy::schema::Schema;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use crate::search::botsearch::QueryError::Parsing;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn card_query_parser(index: &Index) -> QueryParser {
|
||||||
|
let schema = index.schema();
|
||||||
|
|
||||||
|
let f_code = schema.get_field("code").expect("schema to have a 'code' field");
|
||||||
|
let f_name = schema.get_field("name").expect("schema to have a 'name' field");
|
||||||
|
let f_type = schema.get_field("type").expect("schema to have a 'type' field");
|
||||||
|
let f_set = schema.get_field("set").expect("schema to have a 'set' field");
|
||||||
|
let f_rarity = schema.get_field("rarity").expect("schema to have a 'rarity' field");
|
||||||
|
let f_collectible = schema.get_field("collectible").expect("schema to have a 'collectible' field");
|
||||||
|
let f_regions = schema.get_field("regions").expect("schema to have a 'regions' field");
|
||||||
|
let f_attack = schema.get_field("attack").expect("schema to have a 'attack' field");
|
||||||
|
let f_cost = schema.get_field("cost").expect("schema to have a 'cost' field");
|
||||||
|
let f_health = schema.get_field("health").expect("schema to have a 'health' field");
|
||||||
|
let f_spellspeed = schema.get_field("spellspeed").expect("schema to have a 'spellspeed' field");
|
||||||
|
let f_keywords = schema.get_field("keywords").expect("schema to have a 'keywords' field");
|
||||||
|
let f_description = schema.get_field("description").expect("schema to have a 'description' field");
|
||||||
|
let f_levelup = schema.get_field("levelup").expect("schema to have a 'levelup' field");
|
||||||
|
let f_associated = schema.get_field("associated").expect("schema to have a 'associated' field");
|
||||||
|
let f_flavor = schema.get_field("flavor").expect("schema to have a 'flavor' field");
|
||||||
|
let f_artist = schema.get_field("artist").expect("schema to have a 'artist' field");
|
||||||
|
let f_subtypes = schema.get_field("subtypes").expect("schema to have a 'subtypes' field");
|
||||||
|
let f_supertype = schema.get_field("supertype").expect("schema to have a 'supertype' field");
|
||||||
|
|
||||||
|
QueryParser::for_index(
|
||||||
|
&index,
|
||||||
|
vec![
|
||||||
|
f_code,
|
||||||
|
f_name,
|
||||||
|
f_type,
|
||||||
|
f_set,
|
||||||
|
f_rarity,
|
||||||
|
f_collectible,
|
||||||
|
f_regions,
|
||||||
|
f_attack,
|
||||||
|
f_cost,
|
||||||
|
f_health,
|
||||||
|
f_spellspeed,
|
||||||
|
f_keywords,
|
||||||
|
f_description,
|
||||||
|
f_levelup,
|
||||||
|
f_associated,
|
||||||
|
f_flavor,
|
||||||
|
f_artist,
|
||||||
|
f_subtypes,
|
||||||
|
f_supertype,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub enum QueryError {
|
||||||
|
Parsing(QueryParserError),
|
||||||
|
Search(TantivyError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn card_query(schema: &Schema, reader: &IndexReader, parser: &QueryParser, query: &str, amount: usize) -> Result<Vec<String>, QueryError> {
|
||||||
|
log::debug!("Searching for `{}`...", &query);
|
||||||
|
|
||||||
|
let searcher = reader.searcher();
|
||||||
|
let query = parser.parse_query(query)
|
||||||
|
.map_err(QueryError::Parsing)?;
|
||||||
|
let search = searcher.search(&*query, &TopDocs::with_limit(amount))
|
||||||
|
.map_err(QueryError::Search)?;
|
||||||
|
|
||||||
|
let f_code = schema.get_field("code").expect("schema to have a 'code' field");
|
||||||
|
|
||||||
|
let results = search.iter()
|
||||||
|
.filter_map(|(_score, address)| searcher.doc(address.to_owned()).ok())
|
||||||
|
.filter_map(|doc| doc.get_first(f_code).cloned())
|
||||||
|
.filter_map(|field| field.as_text().map(String::from))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
|
@ -26,12 +26,12 @@ pub fn cardcode_options() -> tantivy::schema::TextOptions {
|
||||||
|
|
||||||
|
|
||||||
/// Create a new [tantivy::schema::TextOptions] for card keywords, using the given tokenizer.
|
/// Create a new [tantivy::schema::TextOptions] for card keywords, using the given tokenizer.
|
||||||
pub fn cardkeyword_options(tokenizer_name: &'static str) -> tantivy::schema::TextOptions {
|
pub fn cardkeyword_options() -> tantivy::schema::TextOptions {
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
|
|
||||||
TextOptions::default()
|
TextOptions::default()
|
||||||
.set_indexing_options(TextFieldIndexing::default()
|
.set_indexing_options(TextFieldIndexing::default()
|
||||||
.set_tokenizer(tokenizer_name)
|
.set_tokenizer("card")
|
||||||
.set_fieldnorms(false)
|
.set_fieldnorms(false)
|
||||||
.set_index_option(IndexRecordOption::Basic)
|
.set_index_option(IndexRecordOption::Basic)
|
||||||
)
|
)
|
||||||
|
@ -39,12 +39,12 @@ pub fn cardkeyword_options(tokenizer_name: &'static str) -> tantivy::schema::Tex
|
||||||
|
|
||||||
|
|
||||||
/// Create a new [tantivy::schema::TextOptions] for card text fields, using the given tokenizer.
|
/// Create a new [tantivy::schema::TextOptions] for card text fields, using the given tokenizer.
|
||||||
pub fn cardtext_options(tokenizer_name: &'static str) -> tantivy::schema::TextOptions {
|
pub fn cardtext_options() -> tantivy::schema::TextOptions {
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
|
|
||||||
TextOptions::default()
|
TextOptions::default()
|
||||||
.set_indexing_options(TextFieldIndexing::default()
|
.set_indexing_options(TextFieldIndexing::default()
|
||||||
.set_tokenizer(tokenizer_name)
|
.set_tokenizer("card")
|
||||||
.set_fieldnorms(true)
|
.set_fieldnorms(true)
|
||||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions)
|
.set_index_option(IndexRecordOption::WithFreqsAndPositions)
|
||||||
)
|
)
|
||||||
|
@ -52,14 +52,14 @@ pub fn cardtext_options(tokenizer_name: &'static str) -> tantivy::schema::TextOp
|
||||||
|
|
||||||
|
|
||||||
/// Create a new [tantivy::schema::Schema] using [Card]s as documents.
|
/// Create a new [tantivy::schema::Schema] using [Card]s as documents.
|
||||||
pub fn card_schema(tokenizer_name: &'static str) -> tantivy::schema::Schema {
|
pub fn card_schema() -> tantivy::schema::Schema {
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
|
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|
||||||
let cardcode: TextOptions = cardcode_options();
|
let cardcode: TextOptions = cardcode_options();
|
||||||
let cardkeyword: TextOptions = cardkeyword_options(tokenizer_name);
|
let cardkeyword: TextOptions = cardkeyword_options();
|
||||||
let cardtext: TextOptions = cardtext_options(tokenizer_name);
|
let cardtext: TextOptions = cardtext_options();
|
||||||
|
|
||||||
schema_builder.add_text_field("code", cardcode);
|
schema_builder.add_text_field("code", cardcode);
|
||||||
schema_builder.add_text_field("name", cardtext.clone());
|
schema_builder.add_text_field("name", cardtext.clone());
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
//! This module implements full-text search on Legends of Runeterra data.
|
//! This module implements full-text search on Legends of Runeterra data.
|
||||||
|
|
||||||
pub mod card;
|
pub mod card;
|
||||||
|
pub(crate) mod botindex;
|
||||||
|
pub(crate) mod botsearch;
|
3
src/telegram/bin.rs
Normal file
3
src/telegram/bin.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
|
||||||
|
}
|
82
src/telegram/display.rs
Normal file
82
src/telegram/display.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//! This module defines functions to convert [patched-porobot] structs to [String]s formatted with [Telegram Bot HTML](https://core.telegram.org/bots/api#html-style).
|
||||||
|
//!
|
||||||
|
//! TODO: Add support for non-latin languages.
|
||||||
|
//!
|
||||||
|
//! TODO: Preferably refactor everything in here, as the code is poor quality.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use teloxide::utils::html::escape;
|
||||||
|
use crate::load::corebundle::MappedGlobals;
|
||||||
|
use crate::schema::corebundle::{CoreRegion, CoreSet};
|
||||||
|
use crate::schema::setbundle::{Card, CardRegion, CardSet, CardType};
|
||||||
|
|
||||||
|
|
||||||
|
/// Render a [Card] to a [String] formatted with [Telegram Bot HTML](https://core.telegram.org/bots/api#html-style).
|
||||||
|
pub fn display_card(card: &Card, mg: &MappedGlobals) -> String {
|
||||||
|
let title = format!(r#"<a href="{}"><b><i>{}</b></i></a>"#, &card.main_art().card_png, escape(&card.name));
|
||||||
|
|
||||||
|
let stats = match &card.r#type {
|
||||||
|
CardType::Spell => format!("{} mana", escape(&card.cost.to_string())),
|
||||||
|
CardType::Unit => format!("{} mana {}|{}", escape(&card.cost.to_string()), escape(&card.attack.to_string()), escape(&card.health.to_string())),
|
||||||
|
CardType::Ability => "".to_string(),
|
||||||
|
CardType::Landmark => format!("{} mana", &card.cost),
|
||||||
|
CardType::Trap => "".to_string(),
|
||||||
|
CardType::Unsupported => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let set = format!("<i>{}</i>", escape(&display_set(card.set, &mg.sets)));
|
||||||
|
let regions = format!("<i>{}</i>", escape(&display_regions(&card.regions, &mg.regions)));
|
||||||
|
let r#type = format!("<i>{}</i>", escape(&display_type(card.r#type)));
|
||||||
|
|
||||||
|
let breadcrumbs = format!("{} › {} › {}", &set, ®ions, &r#type);
|
||||||
|
|
||||||
|
let description = card.localized_description_text.clone();
|
||||||
|
let flavor = format!("<i>{}</i>", &card.localized_flavor_text);
|
||||||
|
let artist = format!(r#"<a href="{}">Art by {}</a>"#, &card.main_art().full_png, &card.artist_name);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{title} {stats}\n{breadcrumbs}\n\n{description}\n\n-----\n{flavor}\n\n{artist}",
|
||||||
|
title=title,
|
||||||
|
stats=stats,
|
||||||
|
breadcrumbs=breadcrumbs,
|
||||||
|
description=description,
|
||||||
|
flavor=flavor,
|
||||||
|
artist=artist,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Render a [CardSet] to a [String].
|
||||||
|
fn display_set(set: CardSet, hm: &HashMap<CardSet, CoreSet>) -> String {
|
||||||
|
set
|
||||||
|
.localized(&hm)
|
||||||
|
.map(|o| o.name.clone())
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Render a slice of [CardRegion]s to a [String].
|
||||||
|
fn display_regions(regions: &[CardRegion], hm: &HashMap<CardRegion, CoreRegion>) -> String {
|
||||||
|
regions
|
||||||
|
.iter()
|
||||||
|
.map(|region| region
|
||||||
|
.localized(&hm)
|
||||||
|
.map(|o| o.name.clone())
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string())
|
||||||
|
)
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Render a [CardType] to a [String].
|
||||||
|
fn display_type(r#type: CardType) -> String {
|
||||||
|
match r#type {
|
||||||
|
CardType::Spell => "Spell",
|
||||||
|
CardType::Unit => "Unit",
|
||||||
|
CardType::Ability => "Ability",
|
||||||
|
CardType::Landmark => "Landmark",
|
||||||
|
CardType::Trap => "Trap",
|
||||||
|
CardType::Unsupported => "Unknown",
|
||||||
|
}.to_string()
|
||||||
|
}
|
2
src/telegram/mod.rs
Normal file
2
src/telegram/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub(crate) mod display;
|
||||||
|
mod bin;
|
Loading…
Reference in a new issue