mirror of
https://github.com/Steffo99/patched-porobot.git
synced 2024-12-23 01:54:22 +00:00
Batch of changes for v0.9.0
(#7)
This commit is contained in:
parent
e2afa112d8
commit
a0974f91d5
20 changed files with 1487 additions and 386 deletions
|
@ -3,7 +3,7 @@
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
<Languages>
|
<Languages>
|
||||||
<language minSize="67" name="Rust" />
|
<language minSize="79" name="Rust" />
|
||||||
</Languages>
|
</Languages>
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="PWA">
|
||||||
|
<option name="wasEnabledAtLeastOnce" value="true" />
|
||||||
|
</component>
|
||||||
<component name="ProjectRootManager">
|
<component name="ProjectRootManager">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
|
|
1065
Cargo.lock
generated
1065
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "patched_porobot"
|
name = "patched_porobot"
|
||||||
version = "0.8.0"
|
version = "0.9.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"
|
||||||
|
@ -11,7 +11,7 @@ categories = ["games", "parser-implementations"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
cargo-args = ["--bins"]
|
cargo-args = ["--bins", "--lib"]
|
||||||
rustdoc-args = ["--document-private-items"]
|
rustdoc-args = ["--document-private-items"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,14 +30,16 @@ pretty_env_logger = { version = "0.4.0", optional = true }
|
||||||
serde = { version = "1.0.140", features = ["derive"] }
|
serde = { version = "1.0.140", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.82" }
|
serde_json = { version = "1.0.82" }
|
||||||
# search
|
# search
|
||||||
tantivy = { version = "0.18.0", optional = true }
|
tantivy = { version = "0.19.1", optional = true }
|
||||||
# telegram
|
# telegram
|
||||||
teloxide = { version = "0.10.1", optional = true }
|
teloxide = { version = "0.12.0", optional = true }
|
||||||
reqwest = { version = "0.11.11", optional = true }
|
reqwest = { version = "0.11.11", optional = true }
|
||||||
tokio = { version = "1.20.3", features = ["rt-multi-thread", "macros"], optional = true }
|
tokio = { version = "1.20.3", features = ["rt-multi-thread", "macros"], optional = true }
|
||||||
md5 = { version = "0.7.0", optional = true }
|
md5 = { version = "0.7.0", optional = true }
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
# discord
|
# discord
|
||||||
|
serenity = { version = "0.11.5", features = ["client", "cache", "gateway", "rustls_backend", "model"], default-features = false, optional = true }
|
||||||
|
anyhow = { version = "^1.0.68", optional = true }
|
||||||
# matrix
|
# matrix
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ rand = { version = "0.8.5", optional = true }
|
||||||
exec = ["pretty_env_logger"]
|
exec = ["pretty_env_logger"]
|
||||||
search = ["tantivy"]
|
search = ["tantivy"]
|
||||||
telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5", "rand"]
|
telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5", "rand"]
|
||||||
discord = ["exec", "search"]
|
discord = ["exec", "search", "serenity", "tokio", "anyhow"]
|
||||||
matrix = ["exec", "search"]
|
matrix = ["exec", "search"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,119 @@
|
||||||
//! This bot isn't yet available.
|
//! # [Patched Porobot#7556]
|
||||||
|
//!
|
||||||
|
//! Bot for searching and sending Legends of Runeterra cards and decks in Discord channels
|
||||||
|
//!
|
||||||
|
//! ## Usage
|
||||||
|
//!
|
||||||
|
//! [Patched Porobot#7556] is based on slash commands; you can add it to your server by clicking on the following link:
|
||||||
|
//!
|
||||||
|
//! * [Add to Server](https://discord.com/api/oauth2/authorize?client_id=1071989978743193672&scope=applications.commands)
|
||||||
|
//!
|
||||||
|
//! After adding it to your server, you can use its commands in channels by entering `/` in the message box and then selecting the command you want to send to the bot.
|
||||||
|
//!
|
||||||
|
//! ### Card queries
|
||||||
|
//!
|
||||||
|
//! You can search for a card by specifying the `/card` command and inserting the card's name as the `query` parameter:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:shadowshift
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! You can specify multiple words to find the card that contains all of them:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:mighty poro
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Terms will be searched in multiple fields, so to find [Daring Poro](https://leagueoflegends.fandom.com/wiki/Daring_Poro_(Legends_of_Runeterra)) you may search for:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:piltover poro
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Since different Champion cards have the same name, you may disambiguate them by entering `level:N` as part of the query:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! /card query:braum level:2
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! #### Conjunctions
|
||||||
|
//!
|
||||||
|
//! By default, all terms in the query are joined by `AND` conjuctions, meaning that only cards containing **all** of the terms are retrieved.
|
||||||
|
//!
|
||||||
|
//! If you want to find cards matching **any** of the terms, you'll have to manually join them with the `OR` conjuction:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:progress OR heimerdinger
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To have both `AND`s and `OR`s in the same query you'll need to specify all of them explicitly:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:von AND yipp OR cat
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! #### Fields
|
||||||
|
//!
|
||||||
|
//! You can perform searches about specific card properties:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:cost:4 attack:7 health:7
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Conjunctions are supported even when searching by specific fields:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /card query:name:Bard OR description:Chime
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ##### Supported fields
|
||||||
|
//!
|
||||||
|
//! [Patched Porobot#7556] supports the various fields for searching cards: see [**this table**](patched_porobot::search::cardsearch::CardSearchEngine::schema) for a list of all of them!
|
||||||
|
//!
|
||||||
|
//! #### Ranges
|
||||||
|
//!
|
||||||
|
//! Finally, you can request specific ranges for your search using square brackets and the `TO` keyword:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! @patchedporobot attack:[8 TO 12]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! #### Query parser
|
||||||
|
//!
|
||||||
|
//! Since [Patched Porobot#7556] uses [`tantivy`] internally, you might find more information on even more advanced queries in the [documentation of their `QueryParser`](tantivy::query::QueryParser)!
|
||||||
|
//!
|
||||||
|
//! ### Deck parsing
|
||||||
|
//!
|
||||||
|
//! You can have [Patched Porobot#7556] display a deck and its cards by specifying the `/deck` command and inserting the deck code as the `code` parameter:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /deck code:CIBQCAICAQAQGBQIBEBAMBAJBMGBUHJNGE4AEAIBAIYQEAQGEU2QCAIBAIUQ
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! #### Named decks
|
||||||
|
//!
|
||||||
|
//! Optionally, you may add a name to your deck, which will be displayed above the deck code:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! /deck code:CIBQCAICAQAQGBQIBEBAMBAJBMGBUHJNGE4AEAIBAIYQEAQGEU2QCAIBAIUQ name:Gimbo's Depths
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ### Permissions
|
||||||
|
//!
|
||||||
|
//! You can configure the bot's permissions by using Discord's Command Permissions system!
|
||||||
|
//!
|
||||||
|
//! You can access it via `SERVER NAME` → `Server Settings` → `Integrations` → [Patched Porobot#7556] `Manage`.
|
||||||
|
//!
|
||||||
|
//! See the following flowchart to understand how Command Permissions work:
|
||||||
|
//!
|
||||||
|
//! * [Flowchart](https://cdn.discordapp.com/attachments/697138785317814292/1042878162901672048/flowchart-for-new-permissions.png)
|
||||||
|
//!
|
||||||
|
//! [Patched Porobot#7556]: https://discord.com/api/oauth2/authorize?client_id=1071989978743193672&scope=applications.commands
|
||||||
|
|
||||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/patched-porobot/main/icon.png")]
|
#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/patched-porobot/main/icon.png")]
|
||||||
|
|
||||||
fn main() {
|
#[doc(hidden)]
|
||||||
todo!();
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
patched_porobot::discord::main::main().await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,13 @@ impl CardCode {
|
||||||
/// The token segment of the code.
|
/// The token segment of the code.
|
||||||
///
|
///
|
||||||
/// In valid codes, it may either be an empty string, or 2-ASCII-characters long.
|
/// In valid codes, it may either be an empty string, or 2-ASCII-characters long.
|
||||||
pub fn token(&self) -> &str {
|
pub fn token(&self) -> Option<&str> {
|
||||||
&self.full[7..9]
|
if self.full.len() >= 9 {
|
||||||
|
Some(&self.full[7..9])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new card code given the set and region strings and the card number.
|
/// Create a new card code given the set and region strings and the card number.
|
||||||
|
|
|
@ -359,6 +359,85 @@ impl CardKeyword {
|
||||||
) -> Option<&'hm LocalizedCardKeyword> {
|
) -> Option<&'hm LocalizedCardKeyword> {
|
||||||
hm.get(self)
|
hm.get(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the Discord emoji code associated with this [`CardKeyword`].
|
||||||
|
pub fn discord_emoji(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CardKeyword::DoubleAttack => "<:doublestrike:1056023011590942770>",
|
||||||
|
CardKeyword::Ephemeral => "<:ephemeral:1056023006876545105>",
|
||||||
|
CardKeyword::Equipment => "<:equipment:1056022999184183317>",
|
||||||
|
CardKeyword::Fast => "<:fast:1056022992536219728>",
|
||||||
|
CardKeyword::Fated => "<:fated:1056022989507940414>",
|
||||||
|
CardKeyword::Fearsome => "<:fearsome:1056022987041673367>",
|
||||||
|
CardKeyword::Focus => "<:focus:1056022982377615390>",
|
||||||
|
CardKeyword::Formidable => "<:formidable:1056022980007837826>",
|
||||||
|
CardKeyword::Frostbite => "<:frostbite:1056022975436038164>",
|
||||||
|
CardKeyword::Fury => "<:fury:1056022973636694076>",
|
||||||
|
CardKeyword::ClobberNoEmptySlotRequirement => "",
|
||||||
|
CardKeyword::Hallowed => "<:hallowed:1056022965914968074>",
|
||||||
|
CardKeyword::Immobile => "<:immobile:1056022961582248026>",
|
||||||
|
CardKeyword::Impact => "<:impact:1056022959279591485>",
|
||||||
|
CardKeyword::Landmark => "<:landmarkvisualonly:1056022936080883783>",
|
||||||
|
CardKeyword::LastBreath => "<:lastbreath:1056022933736263680>",
|
||||||
|
CardKeyword::Lifesteal => "<:lifesteal:1056022931395842160>",
|
||||||
|
CardKeyword::Lurk => "<:lurker:1056022929357426740>",
|
||||||
|
CardKeyword::Overwhelm => "<:overwhelm:1056022921639907420>",
|
||||||
|
CardKeyword::SpellOverwhelm => "<:overwhelm:1056022921639907420>",
|
||||||
|
CardKeyword::QuickAttack => "<:quickstrike:1056022909535125548>",
|
||||||
|
CardKeyword::Regeneration => "<:regeneration:1056022897396809769>",
|
||||||
|
CardKeyword::CantBlock => "<:reckless:1056022900651602092>",
|
||||||
|
CardKeyword::Scout => "<:scout:1056022889389903962>",
|
||||||
|
CardKeyword::Silenced => "<:silenced:1056022882121158657>",
|
||||||
|
CardKeyword::SilenceIndividualKeyword => "<:silenced:1056022882121158657>",
|
||||||
|
CardKeyword::Skill => "<:skillmark:1056022880158228600>",
|
||||||
|
CardKeyword::Slow => "<:slow:1056022877868142662>",
|
||||||
|
CardKeyword::SpellShield => "<:spellshield:1056022875565465640>",
|
||||||
|
CardKeyword::Stun => "<:stunned:1056022873279561759>",
|
||||||
|
CardKeyword::Tough => "<:tough:1056022863066431591>",
|
||||||
|
CardKeyword::Vulnerable => "<:vulnerable:1056022853411143691>",
|
||||||
|
CardKeyword::Attach => "<:attach:1056024270712614974>",
|
||||||
|
CardKeyword::Attune => "<:attune:1056024273401171999>",
|
||||||
|
CardKeyword::Augment => "<:augment:1056024275628335114>",
|
||||||
|
CardKeyword::AuraVisualFakeKeyword => "<:aura:1056024278212038756>",
|
||||||
|
CardKeyword::Barrier => "<:barrier:1056024286177013900>",
|
||||||
|
CardKeyword::Burst => "<:burst:1056024291457638492>",
|
||||||
|
// CardKeyword::??? => "<:capture:1056024295190577153>",
|
||||||
|
CardKeyword::Challenger => "<:challenger:1056024299179347988>",
|
||||||
|
CardKeyword::Deep => "<:deep:1056024321593720923>",
|
||||||
|
CardKeyword::Elusive => "<:elusive:1056024324110299176>",
|
||||||
|
CardKeyword::Evolve => "<:evolve:1056024326572355654>",
|
||||||
|
CardKeyword::Fleeting => "<:fleeting:1056024328753397862>",
|
||||||
|
CardKeyword::Imbue => "<:imbue:1056024724314001449>",
|
||||||
|
CardKeyword::Countdown => "<:fleeting:1056024328753397862>", // TODO: Is this correct?
|
||||||
|
CardKeyword::OnPlay => "<:skillmark:1056022880158228600>",
|
||||||
|
CardKeyword::Shurima => "<:shurima:1056022884616765500>",
|
||||||
|
CardKeyword::Noxus => "<:noxus:1056022924169064498>",
|
||||||
|
CardKeyword::Demacia => "<:demacia:1056023014128484412>",
|
||||||
|
CardKeyword::Runeterra => "<:runeterra:1056022895031238727>",
|
||||||
|
CardKeyword::Targon => "<:targon:1056022866174418944>",
|
||||||
|
CardKeyword::ShadowIsles => "<:shadowisles:1056022886848135292>",
|
||||||
|
CardKeyword::PiltoverZaun => "<:piltoverzaun:1056022918959734835>",
|
||||||
|
CardKeyword::Ionia => "<:ionia:1056022949569777708>",
|
||||||
|
CardKeyword::BandleCity => "<:bandlecity:1056024280493735976>",
|
||||||
|
CardKeyword::Bilgewater => "<:bilgewater:1056024288215437484>",
|
||||||
|
CardKeyword::Nab => "",
|
||||||
|
CardKeyword::Enlightened => "",
|
||||||
|
CardKeyword::Invoke => "",
|
||||||
|
CardKeyword::Boon => "",
|
||||||
|
CardKeyword::Trap => "",
|
||||||
|
CardKeyword::Drain => "",
|
||||||
|
CardKeyword::Recall => "",
|
||||||
|
CardKeyword::Weakest => "",
|
||||||
|
CardKeyword::Support => "",
|
||||||
|
CardKeyword::Obliterate => "",
|
||||||
|
CardKeyword::Nightfall => "",
|
||||||
|
CardKeyword::Daybreak => "",
|
||||||
|
CardKeyword::Plunder => "",
|
||||||
|
CardKeyword::BlockElusive => "",
|
||||||
|
CardKeyword::Flow => "",
|
||||||
|
CardKeyword::Unsupported => "<:invaliddeck:1056022952396730438>",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -89,6 +89,24 @@ impl CardRegion {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the Discord emoji code associated with this [`CardRegion`].
|
||||||
|
pub fn discord_emoji(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CardRegion::Noxus => "<:noxus:1056022924169064498>",
|
||||||
|
CardRegion::Demacia => "<:demacia:1056023014128484412>",
|
||||||
|
CardRegion::Freljord => "<:freljord:1056024331437735936>",
|
||||||
|
CardRegion::ShadowIsles => "<:shadowisles:1056022886848135292>",
|
||||||
|
CardRegion::Targon => "<:targon:1056022866174418944>",
|
||||||
|
CardRegion::Ionia => "<:ionia:1056022949569777708>",
|
||||||
|
CardRegion::Bilgewater => "<:bilgewater:1056024288215437484>",
|
||||||
|
CardRegion::Shurima => "<:shurima:1056022884616765500>",
|
||||||
|
CardRegion::PiltoverZaun => "<:piltoverzaun:1056022918959734835>",
|
||||||
|
CardRegion::BandleCity => "<:bandlecity:1056024280493735976>",
|
||||||
|
CardRegion::Runeterra => "<:runeterra:1056022895031238727>",
|
||||||
|
CardRegion::Unsupported => "<:invaliddeck:1056022952396730438>",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [`CardRegion`] from its internal id.
|
/// Get the [`CardRegion`] from its internal id.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use crate::data::corebundle::speed::{LocalizedSpellSpeed, LocalizedSpellSpeedIndex};
|
use crate::data::corebundle::speed::{LocalizedSpellSpeed, LocalizedSpellSpeedIndex};
|
||||||
|
|
||||||
/// A possible [`Spell`](super::r#type::CardType::Spell) speed.
|
/// A possible [`Spell`](super::type::CardType::Spell) speed.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum SpellSpeed {
|
pub enum SpellSpeed {
|
||||||
/// Non-spell cards have this speed.
|
/// Non-spell cards have this speed.
|
||||||
|
|
|
@ -15,6 +15,22 @@ pub enum CardSupertype {
|
||||||
Unsupported,
|
Unsupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&CardSupertype> for &'static str {
|
||||||
|
fn from(cs: &CardSupertype) -> Self {
|
||||||
|
match cs {
|
||||||
|
CardSupertype::None => "",
|
||||||
|
CardSupertype::Champion => "Champion",
|
||||||
|
CardSupertype::Unsupported => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&CardSupertype> for String {
|
||||||
|
fn from(cs: &CardSupertype) -> Self {
|
||||||
|
<&CardSupertype as Into<&'static str>>::into(cs).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -30,19 +30,25 @@ pub enum CardType {
|
||||||
Unsupported,
|
Unsupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&CardType> for String {
|
impl From<&CardType> for &'static str {
|
||||||
fn from(r#type: &CardType) -> Self {
|
fn from(r#type: &CardType) -> Self {
|
||||||
match r#type {
|
match r#type {
|
||||||
CardType::Spell => String::from("Spell"),
|
CardType::Spell => "Spell",
|
||||||
CardType::Unit => String::from("Unit"),
|
CardType::Unit => "Unit",
|
||||||
CardType::Ability => String::from("Ability"),
|
CardType::Ability => "Ability",
|
||||||
CardType::Landmark => String::from("Landmark"),
|
CardType::Landmark => "Landmark",
|
||||||
CardType::Trap => String::from("Trap"),
|
CardType::Trap => "Trap",
|
||||||
CardType::Unsupported => String::from("Unknown"),
|
CardType::Unsupported => "Unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&CardType> for String {
|
||||||
|
fn from(cs: &CardType) -> Self {
|
||||||
|
<&CardType as Into<&'static str>>::into(cs).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::CardType;
|
use super::CardType;
|
||||||
|
|
393
src/discord/handler.rs
Normal file
393
src/discord/handler.rs
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
//! Module containing the [`EventHandler`] and its associated functions.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::env;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serenity::builder::EditInteractionResponse;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use serenity::model::prelude::*;
|
||||||
|
use serenity::model::application::interaction::{InteractionResponseType, Interaction};
|
||||||
|
use serenity::model::application::interaction::application_command::CommandDataOptionValue;
|
||||||
|
use crate::data::deckcode::deck::Deck;
|
||||||
|
use crate::data::deckcode::format::DeckCodeFormat;
|
||||||
|
use crate::data::setbundle::r#type::CardType;
|
||||||
|
use crate::data::setbundle::rarity::CardRarity;
|
||||||
|
use crate::data::setbundle::region::CardRegion;
|
||||||
|
use crate::data::setbundle::set::CardSet;
|
||||||
|
use crate::data::setbundle::supertype::CardSupertype;
|
||||||
|
use crate::search::cardsearch::CardSearchEngine;
|
||||||
|
|
||||||
|
/// Event handler for the bot.
|
||||||
|
///
|
||||||
|
/// Contains the functions that process events received by Discord.
|
||||||
|
pub struct EventHandler;
|
||||||
|
|
||||||
|
const WELCOME_MESSAGE: &str = r#"
|
||||||
|
👋 Hi! I'm a robotic poro who can search for Legends of Runeterra cards to send them in chats!
|
||||||
|
|
||||||
|
To search for a card, enter `/card` in a channel where I am enabled, then specify **your search query** as the `query` parameter, like this:
|
||||||
|
```text
|
||||||
|
/card query:mighty poro
|
||||||
|
```
|
||||||
|
After a while, I'll send in the channel the best match I can find for your query!
|
||||||
|
|
||||||
|
You can also perform more **complex queries**, such as this one:
|
||||||
|
```text
|
||||||
|
/card query:cost:4 AND attack:7 AND health:7
|
||||||
|
```
|
||||||
|
To read all details on the queries you can ask me to perform, visit the documentation at: <https://docs.rs/patched_porobot/latest/patched_porobot_telegram>
|
||||||
|
|
||||||
|
Additionally, you can send me the `/deck` command together with a deck code to send the full deck details in chat, like this:
|
||||||
|
```
|
||||||
|
/deck code:CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA
|
||||||
|
```
|
||||||
|
Have a fun time playing Legends of Runeterra!
|
||||||
|
|
||||||
|
_Patched Porobot isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc._
|
||||||
|
"#;
|
||||||
|
|
||||||
|
impl EventHandler {
|
||||||
|
/// Handle the `/help` command.
|
||||||
|
pub fn command_help(response: &mut EditInteractionResponse) -> &mut EditInteractionResponse {
|
||||||
|
response.content(WELCOME_MESSAGE);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the `/card` command.
|
||||||
|
pub fn command_card<'r>(ctx: &Context, response: &'r mut EditInteractionResponse, options: HashMap<String, Option<CommandDataOptionValue>>) -> &'r mut EditInteractionResponse {
|
||||||
|
let typemap = ctx.data.try_read().expect("to be able to acquire read lock on CardSearchEngine");
|
||||||
|
let engine = typemap.get::<CardSearchEngine>().expect("CardSearchEngine to be in the TypeMap");
|
||||||
|
|
||||||
|
let query = match options.get("query") {
|
||||||
|
Some(q) => q,
|
||||||
|
None => return response.content(":warning: Missing `query` parameter."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = match query {
|
||||||
|
Some(q) => q,
|
||||||
|
None => return response.content(":warning: Empty `query` parameter."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = match query {
|
||||||
|
CommandDataOptionValue::String(q) => q,
|
||||||
|
_ => return response.content(":warning: Invalid `query` parameter type."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match engine.query(query, 1) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return response.content(":warning: Invalid card search query syntax."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = result.get(0);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Some(card) => {
|
||||||
|
let response = match card.main_art() {
|
||||||
|
Some(art) => response.content(art.card_png.clone()),
|
||||||
|
None => response.content(card.name.clone()),
|
||||||
|
};
|
||||||
|
let response = response.embed(|e| {
|
||||||
|
e.title(card.name.clone());
|
||||||
|
|
||||||
|
if !card.localized_description_text.is_empty() {
|
||||||
|
e.description(card.localized_description_text.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !card.keywords.is_empty() {
|
||||||
|
e.field("Keywords", card.keywords.iter().map(|r|
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
r.discord_emoji(),
|
||||||
|
r.localized(&engine.globals.keywords).map_or_else(|| String::from("Missing translation"), |l| l.name.clone())
|
||||||
|
)
|
||||||
|
).join(", "), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.field("Mana cost", format!("{} mana", card.cost), true);
|
||||||
|
|
||||||
|
if card.r#type == CardType::Unit {
|
||||||
|
e.field("Stats", format!("{} | {}", card.attack, card.health), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.field("Types", {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
if card.supertype != CardSupertype::None {
|
||||||
|
vec.push(String::from(&card.supertype));
|
||||||
|
}
|
||||||
|
vec.push(String::from(&card.r#type));
|
||||||
|
for subtype in card.subtypes.iter() {
|
||||||
|
vec.push(subtype.to_owned())
|
||||||
|
};
|
||||||
|
vec
|
||||||
|
}.join(", "), true);
|
||||||
|
|
||||||
|
e.field("Regions", card.regions.iter().map(|r| match r {
|
||||||
|
CardRegion::Noxus => "<:noxus:1056022924169064498> Noxus",
|
||||||
|
CardRegion::Demacia => "<:demacia:1056023014128484412> Demacia",
|
||||||
|
CardRegion::Freljord => "<:freljord:1056024331437735936> Freljord",
|
||||||
|
CardRegion::ShadowIsles => "<:shadowisles:1056022886848135292> Shadow Isles",
|
||||||
|
CardRegion::Targon => "<:targon:1056022866174418944> Targon",
|
||||||
|
CardRegion::Ionia => "<:ionia:1056022949569777708> Ionia",
|
||||||
|
CardRegion::Bilgewater => "<:bilgewater:1056024288215437484> Bilgewater",
|
||||||
|
CardRegion::Shurima => "<:shurima:1056022884616765500> Shurima",
|
||||||
|
CardRegion::PiltoverZaun => "<:piltoverzaun:1056022918959734835> Piltover & Zaun",
|
||||||
|
CardRegion::BandleCity => "<:bandlecity:1056024280493735976> Bandle City",
|
||||||
|
CardRegion::Runeterra => "<:runeterra:1056022895031238727> Runeterra",
|
||||||
|
CardRegion::Unsupported => "<:invaliddeck:1056022952396730438> Unknown",
|
||||||
|
}).join(", "), false);
|
||||||
|
|
||||||
|
e.field("Set", match card.set {
|
||||||
|
CardSet::Foundations => "<:foundations:1071644734667366410> Foundations",
|
||||||
|
CardSet::RisingTides => "<:rising_tides:1071644736126976160> Rising Tides",
|
||||||
|
CardSet::CallOfTheMountain => "<:call_of_the_mountain:1071644738555478076> Call of the Mountain",
|
||||||
|
CardSet::EmpiresOfTheAscended => "<:empires_of_the_ascended:1071644740342255616> Empires of the Ascended",
|
||||||
|
CardSet::BeyondTheBandlewood => "<:beyond_the_bandlewood:1071644742640750734> Beyond the Bandlewood",
|
||||||
|
CardSet::Worldwalker => "<:worldwalker:1071644743798370315> Worldwalker",
|
||||||
|
CardSet::TheDarkinSaga => "<:the_darkin_saga:1071644746411417610> The Darkin Saga",
|
||||||
|
CardSet::Events => "Events", // TODO: Add icon
|
||||||
|
CardSet::Unsupported => "<:invaliddeck:1056022952396730438> Unknown",
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
e.field("Rarity", match card.supertype {
|
||||||
|
CardSupertype::Champion => "<:champion:1056024303856001034> Champion",
|
||||||
|
_ => match card.rarity {
|
||||||
|
CardRarity::None => "None",
|
||||||
|
CardRarity::Common => "<:common:1056024315046412358> Common",
|
||||||
|
CardRarity::Rare => "<:rare:1056022907433799690> Rare",
|
||||||
|
CardRarity::Epic => "<:epic:1056023004028608622> Epic",
|
||||||
|
CardRarity::Champion => "<:champion:1056024303856001034> Champion",
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
e.color(match card.supertype {
|
||||||
|
CardSupertype::Champion => 0x81541f,
|
||||||
|
_ => match card.rarity {
|
||||||
|
CardRarity::None => 0x202225,
|
||||||
|
CardRarity::Common => 0x1e6a49,
|
||||||
|
CardRarity::Rare => 0x244778,
|
||||||
|
CardRarity::Epic => 0x502970,
|
||||||
|
CardRarity::Champion => 0x81541f,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !card.localized_flavor_text.is_empty() {
|
||||||
|
e.footer(|f| f.text(card.localized_flavor_text.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(art) = card.main_art() {
|
||||||
|
e
|
||||||
|
.field("Illustration by", card.artist_name.clone(), false)
|
||||||
|
.image(art.full_png.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
e
|
||||||
|
});
|
||||||
|
response
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
response.content(":warning: No cards found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the `/deck` command.
|
||||||
|
pub fn command_deck<'r>(ctx: &Context, response: &'r mut EditInteractionResponse, options: HashMap<String, Option<CommandDataOptionValue>>) -> &'r mut EditInteractionResponse {
|
||||||
|
let typemap = ctx.data.try_read().expect("to be able to acquire read lock on CardSearchEngine");
|
||||||
|
let engine = typemap.get::<CardSearchEngine>().expect("CardSearchEngine to be in the TypeMap");
|
||||||
|
|
||||||
|
let code = match options.get("code") {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return response.content(":warning: Missing `code` parameter."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let code = match code {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return response.content(":warning: Empty `code` parameter."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let code = match code {
|
||||||
|
CommandDataOptionValue::String(c) => c,
|
||||||
|
_ => return response.content(":warning: Invalid `code` parameter type."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let deck = match Deck::from_code(code) {
|
||||||
|
Ok(deck) => deck,
|
||||||
|
_ => return response.content(":warning: Invalid deck code."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = match options.get("name") {
|
||||||
|
Some(Some(CommandDataOptionValue::String(n))) => Some(n),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
response.content(
|
||||||
|
match name {
|
||||||
|
Some(name) => format!("__**{}**__\n```text\n{}\n```", name, deck.to_code(DeckCodeFormat::F1).expect("to be able to serialize the deck code")),
|
||||||
|
None => format!("```text\n{}\n```", deck.to_code(DeckCodeFormat::F1).expect("to be able to serialize the deck code")),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (format, regions) = if let Some(regions) = deck.standard(&engine.cards) {
|
||||||
|
("<:neutral:1056022926660481094> Standard", regions)
|
||||||
|
} else if let Some(regions) = deck.singleton(&engine.cards) {
|
||||||
|
("<:neutral:1056022926660481094> Singleton", regions)
|
||||||
|
} else {
|
||||||
|
("<:invaliddeck:1056022952396730438> Unknown", HashSet::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
response.embed(|e| {
|
||||||
|
e.description(
|
||||||
|
deck.contents.iter()
|
||||||
|
.map(|(cc, qty)| {
|
||||||
|
(cc.to_card(&engine.cards), qty)
|
||||||
|
})
|
||||||
|
.map(|(c, qty)| {
|
||||||
|
let name = match c {
|
||||||
|
None => String::from("<:invaliddeck:1056022952396730438> Unknown card"),
|
||||||
|
Some(c) => c.name.clone(),
|
||||||
|
};
|
||||||
|
format!("**{}×** {}", qty, name)
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
e.field("Format", format, true);
|
||||||
|
|
||||||
|
if !regions.is_empty() {
|
||||||
|
e.field("Regions",
|
||||||
|
regions
|
||||||
|
.iter()
|
||||||
|
.map(|region| format!(
|
||||||
|
"{} {}",
|
||||||
|
region.discord_emoji(),
|
||||||
|
region.localized(&engine.globals.regions)
|
||||||
|
.map_or_else(|| String::from("Missing translation"), |l| l.name.clone())
|
||||||
|
))
|
||||||
|
.join(", "),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the Slash Commands supported by this bot.
|
||||||
|
///
|
||||||
|
/// If `SERENITY_DEV_GUILD_ID` is set, register them as guild commands to avoid caching, otherwise, register them as global commands.
|
||||||
|
pub async fn register_commands(ctx: &Context) -> anyhow::Result<()> {
|
||||||
|
match env::var("SERENITY_DEV_GUILD_ID") {
|
||||||
|
Ok(guild) => {
|
||||||
|
let guild: u64 = guild.parse().expect("SERENITY_DEV_GUILD_ID to be valid");
|
||||||
|
let guild: GuildId = guild.into();
|
||||||
|
|
||||||
|
guild.create_application_command(&ctx.http, |c| c
|
||||||
|
.name("card")
|
||||||
|
.description("Search and send a card in the chat.")
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("query")
|
||||||
|
.description("The query to send to the card search engine.")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
).await?;
|
||||||
|
guild.create_application_command(&ctx.http, |c| c
|
||||||
|
.name("deck")
|
||||||
|
.description("Send a deck in the chat.")
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("code")
|
||||||
|
.description("The code of the deck to send.")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("name")
|
||||||
|
.description("The name of the deck.")
|
||||||
|
.required(false)
|
||||||
|
)
|
||||||
|
).await?;
|
||||||
|
guild.create_application_command(&ctx.http, |c| c
|
||||||
|
.name("help")
|
||||||
|
.description("View the help message.")
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||||
|
.name("card")
|
||||||
|
.description("Search and send a card in the chat.")
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("query")
|
||||||
|
.description("The query to send to the card search engine.")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
).await?;
|
||||||
|
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||||
|
.name("deck")
|
||||||
|
.description("Send a deck in the chat.")
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("code")
|
||||||
|
.description("The code of the deck to send.")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
.create_option(|o| o
|
||||||
|
.kind(command::CommandOptionType::String)
|
||||||
|
.name("name")
|
||||||
|
.description("The name of the deck.")
|
||||||
|
.required(false)
|
||||||
|
)
|
||||||
|
).await?;
|
||||||
|
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||||
|
.name("help")
|
||||||
|
.description("View the help message.")
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serenity::async_trait]
|
||||||
|
impl serenity::client::EventHandler for EventHandler {
|
||||||
|
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||||
|
log::debug!("Received ready event from the gateway");
|
||||||
|
|
||||||
|
EventHandler::register_commands(&ctx).await.expect("to be able to register commands");
|
||||||
|
|
||||||
|
log::info!("{} is ready!", &ready.user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
match interaction {
|
||||||
|
Interaction::ApplicationCommand(command) => {
|
||||||
|
let cmd_name = command.data.name.as_str();
|
||||||
|
let cmd_opts: HashMap<String, Option<CommandDataOptionValue>> = command.data.options
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|option| (option.name, option.resolved))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
log::info!("Received command: {}", &cmd_name);
|
||||||
|
|
||||||
|
command.create_interaction_response(&ctx.http, |r| r
|
||||||
|
.interaction_response_data(|d| d
|
||||||
|
.ephemeral(cmd_name == "help")
|
||||||
|
)
|
||||||
|
.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||||
|
).await.expect("to be able to defer the response");
|
||||||
|
|
||||||
|
command.edit_original_interaction_response(
|
||||||
|
&ctx.http,
|
||||||
|
|response| match cmd_name {
|
||||||
|
"card" => Self::command_card(&ctx, response, cmd_opts),
|
||||||
|
"deck" => Self::command_deck(&ctx, response, cmd_opts),
|
||||||
|
"help" => Self::command_help(response),
|
||||||
|
_ => response.content(":warning: Unknown command."),
|
||||||
|
}
|
||||||
|
).await.expect("to be able to update the deferred response");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/discord/main.rs
Normal file
41
src/discord/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
//! Module defining the [`main`] function for `patched_porobot_discord`.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use log::*;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use crate::data::corebundle::create_globalindexes_from_wd;
|
||||||
|
use crate::data::setbundle::create_cardindex_from_wd;
|
||||||
|
use crate::discord::handler::EventHandler;
|
||||||
|
use crate::search::cardsearch::CardSearchEngine;
|
||||||
|
|
||||||
|
/// The function that `patched_porobot_discord` should run when it's started.
|
||||||
|
pub async fn main() {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
debug!("Logger initialized successfully!");
|
||||||
|
|
||||||
|
debug!("Creating LocalizedGlobalIndexes...");
|
||||||
|
let globals = create_globalindexes_from_wd();
|
||||||
|
debug!("Created LocalizedGlobalIndexes!");
|
||||||
|
|
||||||
|
debug!("Creating CardIndex...");
|
||||||
|
let cards = create_cardindex_from_wd();
|
||||||
|
debug!("Created CardIndex!");
|
||||||
|
|
||||||
|
debug!("Creating CardSearchEngine...");
|
||||||
|
let engine = CardSearchEngine::new(globals, cards);
|
||||||
|
debug!("Created CardSearchEngine!");
|
||||||
|
|
||||||
|
let token: String = env::var("SERENITY_TOKEN").expect("SERENITY_TOKEN to be set");
|
||||||
|
let appid: u64 = env::var("SERENITY_APPID").expect("SERENITY_APPID to be set")
|
||||||
|
.parse().expect("SERENITY_APPID to be valid");
|
||||||
|
|
||||||
|
Client::builder(&token, GatewayIntents::non_privileged())
|
||||||
|
.event_handler(EventHandler)
|
||||||
|
.type_map_insert::<CardSearchEngine>(engine)
|
||||||
|
.application_id(appid)
|
||||||
|
.await
|
||||||
|
.expect("to be able to create the Discord client")
|
||||||
|
.start_autosharded()
|
||||||
|
.await
|
||||||
|
.expect("to be able to start the Discord client");
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
//! Module providing utilities to be used in the `patched_porobot_discord` executable target.
|
//! Module providing utilities to be used in the `patched_porobot_discord` executable target.
|
||||||
//!
|
//!
|
||||||
//! While adding new features to this module, remember that binaries [can only access the public API of the crate](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries), as they considered a separate crate from the rest of the project.
|
//! While adding new features to this module, remember that binaries [can only access the public API of the crate](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries), as they considered a separate crate from the rest of the project.
|
||||||
|
|
||||||
|
pub mod handler;
|
||||||
|
pub mod main;
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
//! Additionally, every one of the following features enables the compilation of an additional binary target:
|
//! Additionally, every one of the following features enables the compilation of an additional binary target:
|
||||||
//!
|
//!
|
||||||
//! - [`telegram`] enables the compilation of `patched_porobot_telegram`, a [Telegram inline bot](https://core.telegram.org/bots/api) allowing users to search and send cards in any Telegram chat;
|
//! - [`telegram`] enables the compilation of `patched_porobot_telegram`, a [Telegram inline bot](https://core.telegram.org/bots/api) allowing users to search and send cards in any Telegram chat;
|
||||||
//! - ~~[`discord`] enables the compilation of `patched_porobot_discord`, a [Discord bot](https://discord.com/developers/docs/intro#bots-and-apps) allowing Discord servers the bot is added to to search and send cards in their channels~~;
|
//! - [`discord`] enables the compilation of `patched_porobot_discord`, a [Discord bot](https://discord.com/developers/docs/intro#bots-and-apps) allowing Discord servers the bot is added to to search and send cards in their channels;
|
||||||
//! - ~~[`matrix`] enables the compilation of `patched_porobot_matrix`, a Matrix bot parsing messages in the rooms where it is added to to send details about the cards mentioned in messages~~.
|
//! - ~~[`matrix`] enables the compilation of `patched_porobot_matrix`, a Matrix bot parsing messages in the rooms where it is added to to send details about the cards mentioned in messages~~.
|
||||||
//!
|
//!
|
||||||
//! # Legal
|
//! # Legal
|
||||||
|
|
|
@ -9,6 +9,8 @@ use tantivy::query::{QueryParser, QueryParserError};
|
||||||
use tantivy::schema::{Field, NumericOptions, Schema, TextOptions};
|
use tantivy::schema::{Field, NumericOptions, Schema, TextOptions};
|
||||||
use tantivy::tokenizer::TextAnalyzer;
|
use tantivy::tokenizer::TextAnalyzer;
|
||||||
use tantivy::{Document, Index, IndexReader, IndexWriter};
|
use tantivy::{Document, Index, IndexReader, IndexWriter};
|
||||||
|
use crate::data::setbundle::r#type::CardType;
|
||||||
|
use crate::data::setbundle::supertype::CardSupertype;
|
||||||
|
|
||||||
/// The search engine.
|
/// The search engine.
|
||||||
///
|
///
|
||||||
|
@ -124,6 +126,7 @@ impl CardSearchEngine {
|
||||||
/// | `flavor` | [text](Self::options_text) | The [flavor text of the card](Card::localized_flavor_text). |
|
/// | `flavor` | [text](Self::options_text) | The [flavor text of the card](Card::localized_flavor_text). |
|
||||||
/// | `artist` | [text](Self::options_text) | The [artist(s) of the card's illustration](Card::artist_name). |
|
/// | `artist` | [text](Self::options_text) | The [artist(s) of the card's illustration](Card::artist_name). |
|
||||||
/// | `subtypes` | [text](Self::options_text) | The [subtypes of the card](Card::subtypes), such as `Poro` or `Yordle`. |
|
/// | `subtypes` | [text](Self::options_text) | The [subtypes of the card](Card::subtypes), such as `Poro` or `Yordle`. |
|
||||||
|
/// | `level` | [number](Self::options_number) | `0` if a non-champion, `1` if not leveled, `2` if leveled, `3` if ascended. |
|
||||||
///
|
///
|
||||||
/// Use [Self::schema_fields] to create the [CardSchemaFields] object containing all of them.
|
/// Use [Self::schema_fields] to create the [CardSchemaFields] object containing all of them.
|
||||||
///
|
///
|
||||||
|
@ -146,14 +149,15 @@ impl CardSearchEngine {
|
||||||
schema_builder.add_text_field("regions", options_keyword.clone());
|
schema_builder.add_text_field("regions", options_keyword.clone());
|
||||||
schema_builder.add_u64_field("attack", options_number.clone());
|
schema_builder.add_u64_field("attack", options_number.clone());
|
||||||
schema_builder.add_u64_field("cost", options_number.clone());
|
schema_builder.add_u64_field("cost", options_number.clone());
|
||||||
schema_builder.add_u64_field("health", options_number);
|
schema_builder.add_u64_field("health", options_number.clone());
|
||||||
schema_builder.add_text_field("spellspeed", options_keyword.clone());
|
schema_builder.add_text_field("spellspeed", options_keyword.clone());
|
||||||
schema_builder.add_text_field("keywords", options_keyword.clone());
|
schema_builder.add_text_field("keywords", options_keyword.clone());
|
||||||
schema_builder.add_text_field("description", options_text.clone());
|
schema_builder.add_text_field("description", options_text.clone());
|
||||||
schema_builder.add_text_field("levelup", options_text.clone());
|
schema_builder.add_text_field("levelup", options_text.clone());
|
||||||
schema_builder.add_text_field("flavor", options_text.clone());
|
schema_builder.add_text_field("flavor", options_text.clone());
|
||||||
schema_builder.add_text_field("artist", options_text);
|
schema_builder.add_text_field("artist", options_text);
|
||||||
schema_builder.add_text_field("subtypes", options_keyword.clone());
|
schema_builder.add_text_field("subtypes", options_keyword);
|
||||||
|
schema_builder.add_u64_field("level", options_number);
|
||||||
|
|
||||||
schema_builder.build()
|
schema_builder.build()
|
||||||
}
|
}
|
||||||
|
@ -212,6 +216,9 @@ impl CardSearchEngine {
|
||||||
subtypes: schema
|
subtypes: schema
|
||||||
.get_field("subtypes")
|
.get_field("subtypes")
|
||||||
.expect("schema to have a 'subtypes' field"),
|
.expect("schema to have a 'subtypes' field"),
|
||||||
|
level: schema
|
||||||
|
.get_field("level")
|
||||||
|
.expect("schema to have a 'level' field"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +254,7 @@ impl CardSearchEngine {
|
||||||
use tantivy::doc;
|
use tantivy::doc;
|
||||||
|
|
||||||
doc!(
|
doc!(
|
||||||
fields.code => card.code.full,
|
fields.code => card.code.clone().full,
|
||||||
fields.name => card.name,
|
fields.name => card.name,
|
||||||
fields.r#type => String::from(&card.r#type),
|
fields.r#type => String::from(&card.r#type),
|
||||||
fields.set => card.set
|
fields.set => card.set
|
||||||
|
@ -258,7 +265,7 @@ impl CardSearchEngine {
|
||||||
.localized(&globals.rarities)
|
.localized(&globals.rarities)
|
||||||
.map(|cr| cr.name.to_owned())
|
.map(|cr| cr.name.to_owned())
|
||||||
.unwrap_or_else(String::new),
|
.unwrap_or_else(String::new),
|
||||||
fields.collectible => if card.collectible {1u64} else {0u64},
|
fields.collectible => u64::from(card.collectible),
|
||||||
fields.regions => card.regions.iter()
|
fields.regions => card.regions.iter()
|
||||||
.map(|region| region
|
.map(|region| region
|
||||||
.localized(&globals.regions)
|
.localized(&globals.regions)
|
||||||
|
@ -279,17 +286,35 @@ impl CardSearchEngine {
|
||||||
.unwrap_or_else(String::new))
|
.unwrap_or_else(String::new))
|
||||||
.join(" "),
|
.join(" "),
|
||||||
fields.description => card.localized_description_text,
|
fields.description => card.localized_description_text,
|
||||||
fields.levelup => card.localized_levelup_text,
|
fields.levelup => card.localized_levelup_text.clone(),
|
||||||
fields.flavor => card.localized_flavor_text,
|
fields.flavor => card.localized_flavor_text,
|
||||||
fields.artist => card.artist_name,
|
fields.artist => card.artist_name,
|
||||||
fields.subtypes => card.subtypes.join(" "),
|
fields.subtypes => card.subtypes.join(" "),
|
||||||
|
fields.level => {
|
||||||
|
if card.r#type != CardType::Unit || card.supertype != CardSupertype::Champion {
|
||||||
|
0u64
|
||||||
|
}
|
||||||
|
else if card.subtypes.contains(&"ASCENDED".to_string()) {
|
||||||
|
if card.localized_levelup_text.contains(&"Sun Disc".to_string()) {
|
||||||
|
2u64
|
||||||
|
} else if card.localized_levelup_text.is_empty() {
|
||||||
|
3u64
|
||||||
|
} else {
|
||||||
|
1u64
|
||||||
|
}
|
||||||
|
} else if card.localized_levelup_text.is_empty() {
|
||||||
|
2u64
|
||||||
|
} else {
|
||||||
|
1u64
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the [QueryParser] of the search engine.
|
/// Build the [QueryParser] of the search engine.
|
||||||
fn parser(index: &Index, fields: CardSchemaFields) -> QueryParser {
|
fn parser(index: &Index, fields: CardSchemaFields) -> QueryParser {
|
||||||
let mut parser = QueryParser::for_index(
|
let mut parser = QueryParser::for_index(
|
||||||
&index,
|
index,
|
||||||
vec![
|
vec![
|
||||||
fields.code,
|
fields.code,
|
||||||
fields.name,
|
fields.name,
|
||||||
|
@ -406,4 +431,11 @@ struct CardSchemaFields {
|
||||||
pub artist: Field,
|
pub artist: Field,
|
||||||
/// Space-separated [Card::subtypes].
|
/// Space-separated [Card::subtypes].
|
||||||
pub subtypes: Field,
|
pub subtypes: Field,
|
||||||
|
/// Level of the champion. 0 if not a champion. 1 if not leveled. 2 if leveled. 3 if ascended.
|
||||||
|
pub level: Field,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "discord")]
|
||||||
|
impl serenity::prelude::TypeMapKey for CardSearchEngine {
|
||||||
|
type Value = CardSearchEngine;
|
||||||
}
|
}
|
|
@ -106,14 +106,14 @@ fn display_types(r#type: &CardType, supertype: &CardSupertype, subtypes: &[CardS
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
result.push_str(&*format!("<i>{}</i>", escape(&*String::from(r#type)),));
|
result.push_str(&format!("<i>{}</i>", escape(&String::from(r#type)),));
|
||||||
|
|
||||||
if subtypes.len() > 0 {
|
if !subtypes.is_empty() {
|
||||||
result.push_str(&*format!(
|
result.push_str(&format!(
|
||||||
" › {}",
|
" › {}",
|
||||||
subtypes
|
subtypes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|subtype| format!("<i>{}</i>", escape(&subtype)))
|
.map(|subtype| format!("<i>{}</i>", escape(subtype)))
|
||||||
.join(", ")
|
.join(", ")
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -141,10 +141,10 @@ fn display_keywords(keywords: &[CardKeyword], hm: &LocalizedCardKeywordIndex) ->
|
||||||
///
|
///
|
||||||
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||||
fn display_description(description: &String) -> String {
|
fn display_description(description: &String) -> String {
|
||||||
if description == "" {
|
if description.is_empty() {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{}\n\n", escape(&description))
|
format!("{}\n\n", escape(description))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,10 +152,10 @@ fn display_description(description: &String) -> String {
|
||||||
///
|
///
|
||||||
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||||
fn display_levelup(levelup: &String) -> String {
|
fn display_levelup(levelup: &String) -> String {
|
||||||
if levelup == "" {
|
if levelup.is_empty() {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("<u>Level up</u>: {}\n\n", escape(&levelup))
|
format!("<u>Level up</u>: {}\n\n", escape(levelup))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,10 +204,10 @@ pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&s
|
||||||
|
|
||||||
let mut tags: Vec<&'static str> = vec![];
|
let mut tags: Vec<&'static str> = vec![];
|
||||||
|
|
||||||
let regions = if let Some(regions) = deck.standard(&index) {
|
let regions = if let Some(regions) = deck.standard(index) {
|
||||||
tags.push("#Standard");
|
tags.push("#Standard");
|
||||||
regions
|
regions
|
||||||
} else if let Some(regions) = deck.singleton(&index) {
|
} else if let Some(regions) = deck.singleton(index) {
|
||||||
tags.push("#Singleton");
|
tags.push("#Singleton");
|
||||||
regions
|
regions
|
||||||
} else {
|
} else {
|
||||||
|
@ -232,7 +232,7 @@ pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&s
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags = tags.join(", ");
|
let tags = tags.join(", ");
|
||||||
let tags = if tags.len() > 0 { format!("{}\n", &tags) } else { "".to_string() };
|
let tags = if !tags.is_empty() { format!("{}\n", &tags) } else { "".to_string() };
|
||||||
|
|
||||||
match name {
|
match name {
|
||||||
Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n{}\n{}", &name, &code, &tags, &cards),
|
Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n{}\n{}", &name, &code, &tags, &cards),
|
||||||
|
|
|
@ -14,6 +14,7 @@ use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
/// Handle inline queries by searching cards on the [CardSearchEngine].
|
/// Handle inline queries by searching cards on the [CardSearchEngine].
|
||||||
|
#[allow(clippy::never_loop)]
|
||||||
pub fn inline_query_handler(
|
pub fn inline_query_handler(
|
||||||
crystal: String,
|
crystal: String,
|
||||||
engine: CardSearchEngine,
|
engine: CardSearchEngine,
|
||||||
|
@ -23,10 +24,10 @@ pub fn inline_query_handler(
|
||||||
|
|
||||||
// It's not a real loop, it's just to make the code flow more tolerable.
|
// It's not a real loop, it's just to make the code flow more tolerable.
|
||||||
let payload: AnswerInlineQuery = loop {
|
let payload: AnswerInlineQuery = loop {
|
||||||
if query.query.len() == 0 {
|
if query.query.is_empty() {
|
||||||
debug!("Empty query specified.");
|
debug!("Empty query specified.");
|
||||||
break AnswerInlineQuery {
|
break AnswerInlineQuery {
|
||||||
inline_query_id: query.id.clone(),
|
inline_query_id: query.id,
|
||||||
results: vec![],
|
results: vec![],
|
||||||
cache_time: None,
|
cache_time: None,
|
||||||
is_personal: Some(false),
|
is_personal: Some(false),
|
||||||
|
@ -42,7 +43,7 @@ pub fn inline_query_handler(
|
||||||
|
|
||||||
if let Some(deck_captures) = DECK_RE.captures(&query.query) {
|
if let Some(deck_captures) = DECK_RE.captures(&query.query) {
|
||||||
if let Some(deck_code) = deck_captures.name("code") {
|
if let Some(deck_code) = deck_captures.name("code") {
|
||||||
if let Ok(deck) = Deck::from_code(&deck_code.as_str()) {
|
if let Ok(deck) = Deck::from_code(deck_code.as_str()) {
|
||||||
|
|
||||||
debug!("Parsed deck successfully!");
|
debug!("Parsed deck successfully!");
|
||||||
let name = deck_captures.name("name").map(|m| m.as_str());
|
let name = deck_captures.name("name").map(|m| m.as_str());
|
||||||
|
@ -63,7 +64,7 @@ pub fn inline_query_handler(
|
||||||
debug!("Querying the card search engine...");
|
debug!("Querying the card search engine...");
|
||||||
let results = engine.query(&query.query, 50);
|
let results = engine.query(&query.query, 50);
|
||||||
|
|
||||||
if let Err(_) = results {
|
if results.is_err() {
|
||||||
debug!("Invalid card search query syntax.");
|
debug!("Invalid card search query syntax.");
|
||||||
break AnswerInlineQuery {
|
break AnswerInlineQuery {
|
||||||
inline_query_id: query.id.clone(),
|
inline_query_id: query.id.clone(),
|
||||||
|
@ -118,7 +119,7 @@ pub fn inline_query_handler(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const WELCOME_MESSAGE: &'static str = r#"
|
const WELCOME_MESSAGE: &str = r#"
|
||||||
👋 Hi! I'm a robotic poro who can search for Legends of Runeterra cards to send them in chats!
|
👋 Hi! I'm a robotic poro who can search for Legends of Runeterra cards to send them in chats!
|
||||||
|
|
||||||
To search for a card, enter <b>my username</b> in any chat, followed by <b>your search query</b>, like this:
|
To search for a card, enter <b>my username</b> in any chat, followed by <b>your search query</b>, like this:
|
||||||
|
@ -146,7 +147,7 @@ pub fn message_handler() -> Handler<'static, DependencyMap, ResponseResult<()>,
|
||||||
info!("Handling private message: `{:?}`", &message.text());
|
info!("Handling private message: `{:?}`", &message.text());
|
||||||
|
|
||||||
let payload = SendMessage {
|
let payload = SendMessage {
|
||||||
chat_id: Recipient::Id(message.chat.id.clone()),
|
chat_id: Recipient::Id(message.chat.id),
|
||||||
text: WELCOME_MESSAGE.to_string(),
|
text: WELCOME_MESSAGE.to_string(),
|
||||||
parse_mode: Some(ParseMode::Html),
|
parse_mode: Some(ParseMode::Html),
|
||||||
entities: None,
|
entities: None,
|
||||||
|
@ -156,6 +157,7 @@ pub fn message_handler() -> Handler<'static, DependencyMap, ResponseResult<()>,
|
||||||
reply_to_message_id: None,
|
reply_to_message_id: None,
|
||||||
allow_sending_without_reply: None,
|
allow_sending_without_reply: None,
|
||||||
reply_markup: None,
|
reply_markup: None,
|
||||||
|
message_thread_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
|
|
|
@ -21,7 +21,7 @@ pub fn card_to_inlinequeryresult(
|
||||||
InlineQueryResult::Photo(InlineQueryResultPhoto {
|
InlineQueryResult::Photo(InlineQueryResultPhoto {
|
||||||
id: format!("{}:{}", &crystal, &card.code.full),
|
id: format!("{}:{}", &crystal, &card.code.full),
|
||||||
title: Some(card.name.to_owned()),
|
title: Some(card.name.to_owned()),
|
||||||
caption: Some(display_card(&globals, &card)),
|
caption: Some(display_card(globals, card)),
|
||||||
parse_mode: Some(ParseMode::Html),
|
parse_mode: Some(ParseMode::Html),
|
||||||
photo_url: card
|
photo_url: card
|
||||||
.main_art()
|
.main_art()
|
||||||
|
@ -55,8 +55,6 @@ pub fn deck_to_inlinequeryresult(
|
||||||
.to_code(DeckCodeFormat::F1)
|
.to_code(DeckCodeFormat::F1)
|
||||||
.expect("serialized deck to deserialize properly");
|
.expect("serialized deck to deserialize properly");
|
||||||
|
|
||||||
let message_text = display_deck(index, deck, &code, &name);
|
|
||||||
|
|
||||||
InlineQueryResult::Article(InlineQueryResultArticle {
|
InlineQueryResult::Article(InlineQueryResultArticle {
|
||||||
id: format!("{}:{:x}", &crystal, md5::compute(&code)),
|
id: format!("{}:{:x}", &crystal, md5::compute(&code)),
|
||||||
title: match &name {
|
title: match &name {
|
||||||
|
@ -64,7 +62,7 @@ pub fn deck_to_inlinequeryresult(
|
||||||
None => format!("Deck with {} cards", deck.contents.len())
|
None => format!("Deck with {} cards", deck.contents.len())
|
||||||
},
|
},
|
||||||
input_message_content: InputMessageContent::Text(InputMessageContentText {
|
input_message_content: InputMessageContent::Text(InputMessageContentText {
|
||||||
message_text: display_deck(index, deck, &code, &name),
|
message_text: display_deck(index, deck, &code, name),
|
||||||
parse_mode: Some(ParseMode::Html),
|
parse_mode: Some(ParseMode::Html),
|
||||||
entities: None,
|
entities: None,
|
||||||
disable_web_page_preview: Some(true),
|
disable_web_page_preview: Some(true),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! This module defines the [`main`] function for `patched_porobot_telegram`.
|
//! Module defining the [`main`] function for `patched_porobot_telegram`.
|
||||||
|
|
||||||
use crate::data::corebundle::create_globalindexes_from_wd;
|
use crate::data::corebundle::create_globalindexes_from_wd;
|
||||||
use crate::data::setbundle::create_cardindex_from_wd;
|
use crate::data::setbundle::create_cardindex_from_wd;
|
||||||
|
@ -8,7 +8,7 @@ use log::*;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude::*;
|
||||||
|
|
||||||
/// The main function that `patched_porobot_telegram` should run when it's started.
|
/// The function that `patched_porobot_telegram` should run when it's started.
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
debug!("Logger initialized successfully!");
|
debug!("Logger initialized successfully!");
|
||||||
|
|
Loading…
Reference in a new issue