1
Fork 0
mirror of https://github.com/Steffo99/patched-porobot.git synced 2024-12-22 17:44:22 +00:00

Start more or less clean

This commit is contained in:
Steffo 2022-08-03 15:13:44 +00:00 committed by GitHub
parent 982b70cd71
commit 5836d653a7
11 changed files with 2 additions and 408 deletions

View file

@ -1,45 +0,0 @@
//! This module contains functions to load **Set Bundles** from [Data Dragon](https://developer.riotgames.com/docs/lol).
use log::*;
use itertools::Itertools;
use crate::data::schema::Card;
#[derive(Debug)]
enum LoadingError {
IO(std::io::Error),
Parsing(serde_json::Error),
}
/// Load a single Set Bundle and create a [Vec] with the cards contained in it.
fn load_setbundle(path: std::path::PathBuf) -> Result<Vec<Card>, LoadingError> {
debug!("Loading Set Bundle {:?}", &path);
let file = std::fs::File::open(path)
.map_err(LoadingError::IO)?;
let data = serde_json::de::from_reader::<std::fs::File, Vec<Card>>(file)
.map_err(LoadingError::Parsing)?;
Ok(data)
}
/// Load a single Set Bundle (similarly to [load_setbundle]), but instead of returning a [Result], return an empty [Vec] in case of failure and log a [warn]ing.
fn load_setbundle_infallible(path: std::path::PathBuf) -> Vec<Card> {
match load_setbundle(path) {
Ok(v) => v,
Err(e) => {
log::warn!("{:?}", e);
vec![]
}
}
}
/// Load all Set Bundles matched by the passed glob, using [load_setbundle_infallible] and then concatenating the resulting [Vec]s.
pub fn load_setbundles_infallible(pattern: &str) -> Vec<Card> {
glob::glob(pattern)
.expect("a valid glob")
.filter_map(glob::GlobResult::ok)
.map(load_setbundle_infallible)
.concat()
}

View file

@ -1,14 +0,0 @@
use std::collections::HashMap;
use crate::data::schema::Card;
/// Build a [HashMap] mapping card codes strings to [Card] structs.
pub fn build_card_code_hashmap(cards: &Vec<Card>) -> HashMap<String, Card> {
let mut map = HashMap::<String, Card>::new();
for card in cards {
map.insert(card.card_code.to_owned(), card.to_owned());
}
map
}

View file

@ -1,3 +0,0 @@
pub mod schema;
pub mod load;
pub mod map;

View file

@ -1,71 +1,9 @@
#[macro_use] extern crate tantivy;
use teloxide::prelude::*;
use log::*;
mod data;
mod search;
mod telegram;
mod schema;
/// Run the bot.
#[tokio::main]
async fn main() {
pretty_env_logger::init();
debug!("Loading Set Bundles...");
let cards = data::load::load_setbundles_infallible("./data/*/en_us/data/set*-en_us.json");
debug!("Loaded {} cards!", &cards.len());
debug!("Creating Card index...");
let card_index = search::index::build_card_index();
debug!("Card index created successfully!");
debug!("Creating Card hashmap...");
let card_map = data::map::build_card_code_hashmap(&cards);
debug!("Card hashmap contains {} cards!", &card_map.len());
debug!("Writing Cards to the Card index...");
search::index::write_cards_to_index(&card_index, &cards);
debug!("Wrote Cards to the Card index!");
debug!("Creating Card query parser...");
let card_query_parser = search::query::build_query_parser(&card_index);
debug!("Created Card query parser!");
debug!("Creating Card reader...");
let card_reader = search::index::build_reader(&card_index);
debug!("Created Card reader!");
debug!("Creating Telegram Bot with parameters from the environment...");
let bot = Bot::from_env().auto_send();
let bot_me = bot.get_me().await.expect("Telegram Bot parameters to be valid");
debug!("Created Telegram Bot @{}!", &bot_me.username.as_ref().unwrap());
let handler = Update::filter_inline_query().branch(dptree::endpoint(move |query: InlineQuery, bot: AutoSend<Bot>| {
let card_schema = card_index.schema();
let card_query_parser = &card_query_parser;
let card_reader = &card_reader;
let card_map = &card_map;
let results = search::query::search_card(
&card_schema,
&card_query_parser,
&card_reader,
&card_map,
&query.query
);
async move {
let reply = results.iter()
.map(telegram::result::create_inlinequeryresult);
if let Err(e) = bot.answer_inline_query(&query.id, reply).send().await {
error!("{:?}", e);
};
respond(())
}
}));
Dispatcher::builder(bot, handler).enable_ctrlc_handler().build().dispatch().await;
debug!("There's nothing here yet.")
}

View file

@ -1,41 +0,0 @@
use tantivy::*;
use crate::data::schema::Card;
use crate::search::schema;
/// Build a [tantivy] [Index] storing [Card]s as documents.
pub fn build_card_index() -> Index {
Index::create_in_ram(schema::build_card_schema())
}
/// Build a [tantivy] [IndexWriter] from the given [Index] with some preset parameters.
///
/// Currently allocates 4 MB.
pub fn build_writer(index: &Index) -> IndexWriter {
index.writer(4_000_000)
.expect("to be able to allocate a tantivy writer")
}
/// Write a [Vec] of [Card]s to a [tantivy] [Index], using a writer built with [build_writer].
pub fn write_cards_to_index(index: &Index, cards: &Vec<Card>) -> () {
let schema = index.schema();
let mut writer = build_writer(index);
for card in cards {
let document = schema::card_to_document(&schema, card.to_owned());
writer.add_document(document)
.expect("to be able to add a document to the index");
}
writer.commit()
.expect("to be able to commit the schema changes");
}
/// Build a [tantivy] [IndexReader] from the given [Index] with some preset parameters.
pub fn build_reader(index: &Index) -> IndexReader {
index.reader_builder().reload_policy(ReloadPolicy::OnCommit).try_into()
.expect("to be able to allocate a tantivy reader")
}

View file

@ -1,3 +0,0 @@
pub mod schema;
pub mod index;
pub mod query;

View file

@ -1,49 +0,0 @@
use log::*;
use std::collections::HashMap;
use tantivy::Index;
use tantivy::IndexReader;
use tantivy::LeasedItem;
use tantivy::Searcher;
use tantivy::collector::TopDocs;
use tantivy::query::QueryParser;
use tantivy::schema::Schema;
use crate::data::schema::Card;
use itertools::Itertools;
pub fn build_query_parser(index: &Index) -> QueryParser {
let schema = index.schema();
let name = schema.get_field("name").unwrap();
let description = schema.get_field("description").unwrap();
let code = schema.get_field("code").unwrap();
QueryParser::for_index(&index, vec![name, description, code])
}
pub fn build_searcher(reader: &IndexReader) -> LeasedItem<Searcher> {
reader.searcher()
}
pub fn search_card(schema: &Schema, parser: &QueryParser, reader: &IndexReader, map: &HashMap<String, Card>, q: &str) -> Vec<Card> {
debug!("Searching for `{}`...", &q);
let code = schema.get_field("code").unwrap();
debug!("Building Card searcher...");
let searcher = build_searcher(reader);
let query = parser.parse_query(q)
.expect("to be able to parse the query");
let search = searcher.search(&*query, &TopDocs::with_limit(50))
.expect("to be able to search for a card");
debug!("Retrieved {} results!", &search.len());
search.iter().filter_map(|(_score, address)| searcher.doc(address.to_owned()).ok())
.filter_map(|doc| doc.get_first(code).cloned())
.filter_map(|field| field.as_text().map(String::from))
.filter_map(|code| map.get(&*code))
.cloned()
.collect_vec()
}

View file

@ -1,28 +0,0 @@
use tantivy::schema::*;
use crate::data::schema::Card;
/// Build a [tantivy] [Schema] storing [Card]s as documents.
///
/// TODO: Allow search on all fields.
pub fn build_card_schema() -> Schema {
let mut schema_builder = Schema::builder();
schema_builder.add_text_field("name", TEXT);
schema_builder.add_text_field("description", TEXT);
schema_builder.add_text_field("code", STRING | STORED);
schema_builder.build()
}
/// Convert a [Card] to a Tantivy [Document], using the specified [Schema] (which should come from [build_card_schema]).
pub fn card_to_document(schema: &Schema, card: Card) -> Document {
let name = schema.get_field("name").unwrap();
let description = schema.get_field("description").unwrap();
let code = schema.get_field("code").unwrap();
doc!(
name => card.name,
description => card.description_raw,
code => card.card_code,
)
}

View file

@ -1,134 +0,0 @@
use itertools::Itertools;
use crate::data::schema::{Card, CardType};
pub fn format_card(card: &Card) -> String {
match card.card_type {
CardType::Spell => format!(
"{}{}{}{}\n{}\n{}",
format_name(&card.name),
format_types(&card.supertype, &card.subtypes),
format_keywords(&card.keywords),
format_mana(card.cost),
format_description(&card.description_raw, &card.levelup_description_raw),
format_flavor(&card.assets.get(0).expect("card to have at least one asset").full_absolute_path, &card.flavor_text),
),
CardType::Unit => format!(
"{}{}{}{}{}\n{}\n{}",
format_name(&card.name),
format_types(&card.supertype, &card.subtypes),
format_keywords(&card.keywords),
format_mana(card.cost),
format_stats(card.attack, card.health),
format_description(&card.description_raw, &card.levelup_description_raw),
format_flavor(&card.assets.get(0).expect("card to have at least one asset").full_absolute_path, &card.flavor_text),
),
CardType::Ability => format!(
"{}{}{}\n{}\n{}",
format_name(&card.name),
format_types(&card.supertype, &card.subtypes),
format_keywords(&card.keywords),
format_description(&card.description_raw, &card.levelup_description_raw),
format_flavor(&card.assets.get(0).expect("card to have at least one asset").full_absolute_path, &card.flavor_text),
),
CardType::Landmark => format!(
"{}{}{}{}\n{}\n{}",
format_name(&card.name),
format_types(&card.supertype, &card.subtypes),
format_keywords(&card.keywords),
format_mana(card.cost),
format_description(&card.description_raw, &card.levelup_description_raw),
format_flavor(&card.assets.get(0).expect("card to have at least one asset").full_absolute_path, &card.flavor_text),
),
CardType::Trap => format!(
"{}{}{}\n{}\n{}",
format_name(&card.name),
format_types(&card.supertype, &card.subtypes),
format_keywords(&card.keywords),
format_description(&card.description_raw, &card.levelup_description_raw),
format_flavor(&card.assets.get(0).expect("card to have at least one asset").full_absolute_path, &card.flavor_text),
),
}
}
fn escape(s: &str) -> String {
s
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
fn format_name(name: &String) -> String {
format!("<b><u>{}</u></b>\n", escape(&name))
}
fn format_types(supertype: &String, subtypes: &Vec<String>) -> String {
let mut types = vec![];
if supertype != "" {
types.push(supertype.to_owned())
}
let mut source = subtypes.to_owned();
types.append(&mut source);
let mut types = types.iter()
.map(|t| escape(&t))
.map(|k| k.to_lowercase());
let typestring = types.join(", ");
if typestring == "" {
return String::from("")
}
else {
return format!("<i>{}</i>\n", &typestring)
}
}
fn format_mana(cost: i8) -> String {
format!("{} mana\n", cost)
}
fn format_stats(attack: i8, health: i8) -> String {
format!("{}|{}\n", attack, health)
}
fn format_keywords(keywords: &Vec<String>) -> String {
if keywords.len() == 0 {
return String::from("")
}
else {
format!(
"{}\n",
keywords.iter()
.map(|k| escape(&k))
.map(|k| k.to_lowercase())
.map(|k| format!("[{}]", &k))
.join(" ")
)
}
}
fn format_description(desc: &String, levelup: &String) -> String {
if levelup == "" {
if desc == "" {
String::from("")
}
else {
format!("{}\n", escape(&desc))
}
}
else {
format!("{}\n\n<u>Level up</u>: {}\n", escape(&desc), escape(&levelup))
}
}
fn format_flavor(full_art: &String, flavor: &String) -> String {
format!(r#"<a href="{}"><i>{}</i></a>"#, &full_art, escape(&flavor))
}

View file

@ -1,2 +0,0 @@
pub mod format;
pub mod result;

View file

@ -1,25 +0,0 @@
use crate::data::schema::Card;
use teloxide::types::*;
use reqwest::Url;
use crate::telegram::format::format_card;
pub fn create_inlinequeryresult(card: &Card) -> InlineQueryResult {
InlineQueryResult::Photo(InlineQueryResultPhoto {
id: card.card_code.to_owned(),
title: Some(card.name.to_owned()),
description: Some(card.description_raw.to_owned()),
caption: Some(format_card(&card)),
photo_url: Url::parse(
&card.assets.get(0).expect("card to have assets").game_absolute_path).expect("card to have a valid asset URL"
),
thumb_url: Url::parse(
&card.assets.get(0).expect("card to have assets").game_absolute_path).expect("card to have a valid asset URL"
),
photo_width: Some(680),
photo_height: Some(1024),
parse_mode: Some(ParseMode::Html),
caption_entities: None,
reply_markup: None,
input_message_content: None,
})
}