Better time in events message + README.md and LICENSE.md

This commit is contained in:
Evann Regnault 2023-07-23 13:45:28 +02:00
parent d8fe8edf04
commit 0e43b286ef
7 changed files with 196 additions and 36 deletions

31
Cargo.lock generated
View file

@ -171,7 +171,7 @@ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
"time", "time 0.3.22",
"uuid", "uuid",
] ]
@ -213,8 +213,11 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"time 0.1.45",
"wasm-bindgen",
"winapi", "winapi",
] ]
@ -612,7 +615,7 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -1075,7 +1078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1180,8 +1183,9 @@ dependencies = [
[[package]] [[package]]
name = "obsessed-yanqing" name = "obsessed-yanqing"
version = "1.2.0" version = "1.3.1"
dependencies = [ dependencies = [
"chrono",
"levenshtein", "levenshtein",
"mongodb", "mongodb",
"poise", "poise",
@ -1863,7 +1867,7 @@ dependencies = [
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"time", "time 0.3.22",
"tokio", "tokio",
"tracing", "tracing",
"typemap_rev", "typemap_rev",
@ -2075,6 +2079,17 @@ dependencies = [
"syn 2.0.27", "syn 2.0.27",
] ]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.22" version = "0.3.22"
@ -2412,6 +2427,12 @@ dependencies = [
"try-lock", "try-lock",
] ]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "obsessed-yanqing" name = "obsessed-yanqing"
version = "1.3.0" version = "1.3.1"
edition = "2021" edition = "2021"
authors = ["Evann Regnault"] authors = ["Evann Regnault"]
license = "MIT" license = "MIT"
@ -19,3 +19,4 @@ select = "0.6.0"
regex = "1.9.1" regex = "1.9.1"
rand = "0.8.5" rand = "0.8.5"
mongodb = "2.6.0" mongodb = "2.6.0"
chrono = "0.4.26"

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Evann REGNAULT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
README.md Normal file
View file

@ -0,0 +1,49 @@
# Obsessed Yanqing
This is a bot whose main purpose is to give as much infomrations as possible about Honkai Star Rail.
> ⚠️ Disclaimer : All Data is fetched from [Prydwen.gg](https://prydwen.gg)
## Features
### Event messages
The command `/create_event_message <channel>` creates, as its name indicate, an event message in the channel of your
choice.</br>This message will contain:
- All the currently running banners
- The currently going events
- The upcoming banners and events
- The currently known redeemable codes
> In order to update this message every hour, three information are stored in a database : Guild id, Channel id, Message id.
>
> ❗ Those are the only information stored by the bot !
### Character Data
By using the command `/character <character>`, you get access to loads of data about the selected character.
Such as its rarity, role, description, pros and cons, its different builds, and the recommended teams.
All the navigation is done by click and touch on the buttons bellow the message.
If a character has multiple builds/gears, you need to click on th gear button again to navigate to the next one.
## Setup
If you want to setup this bot by yourself, a docker image is available on [my docker registry](https://registry.evannregnault.dev/#!/taglist/obsessed-yanqing).
You'll also need a mongoDB server alongside it.
### ENV
```env
TOKEN : The discord bot's token
MONGO_HOST : The hostname of the mongodb instance
MONGO_PORT : The port of the mongodb instance
```
## APIs / Libraries
- [Prydwen.gg](https://www.prydwen.gg/star-rail/)
- [Serenity](https://github.com/serenity-rs/serenity)
- [Poise](https://github.com/serenity-rs/poise)

View file

@ -202,7 +202,7 @@ fn create_character_tabs_button<'a>(f: &'a mut CreateComponents, char: &Characte
gear_button.disabled(false); gear_button.disabled(false);
match current_tab { match current_tab {
CharacterTab::Gear(n) => { CharacterTab::Gear(n) => {
gear_button.label(format!("Gear {}/{}", n+1, char.build_data.as_ref().expect("").len())); gear_button.label(format!("Gear {}/{}", n+1, char.build_data.as_ref().expect("Cannot find gear").len()));
} }
_ => {gear_button.label("Gear");} _ => {gear_button.label("Gear");}
} }

View file

@ -1,6 +1,8 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::i32; use std::i32;
use std::str::Split; use std::str::Split;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{NaiveDateTime};
use regex::{Regex}; use regex::{Regex};
use select::document::Document; use select::document::Document;
use select::node::Node; use select::node::Node;
@ -32,11 +34,17 @@ struct BannerData {
four_stars: Vec<String> four_stars: Vec<String>
} }
#[derive(Clone)]
struct EventTime {
start: Option<i64>,
end: Option<i64>
}
#[derive(Clone)] #[derive(Clone)]
struct Event { struct Event {
name: String, name: String,
description: Option<(String, String)>, description: Option<(String, String)>,
time: Option<String>, time: EventTime,
image: Option<String>, image: Option<String>,
banner_data: Option<BannerData>, banner_data: Option<BannerData>,
color: Option<Color>, color: Option<Color>,
@ -49,31 +57,62 @@ struct Code {
} }
fn get_current_events_from_document(doc: &str) -> Vec<Event> { fn get_current_events_from_document(doc: &str) -> Vec<Event> {
let events = get_all_events(doc);
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time no longer works").as_secs() as i64;
events.into_iter().filter(|p| match p.time.start {
None => true,
Some(start) => {
start <= current_time && match p.time.end {
None => true,
Some(end) => current_time < end
}
}
}).collect()
}
fn get_upcoming_events_from_document(doc: &str) -> Vec<Event> {
let events = get_all_events(doc);
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time no longer works").as_secs() as i64;
events.into_iter().filter(|p| match p.time.start {
None => false,
Some(start) => {
start > current_time && match p.time.end {
None => true,
Some(end) => current_time < end
}
}
}).collect()
}
fn get_all_events(doc: &str) -> Vec<Event> {
let document = Document::from(doc); let document = Document::from(doc);
let nodes = document.find(Class("event-tracker")).collect::<Vec<Node>>(); let nodes = document.find(Class("event-tracker")).collect::<Vec<Node>>();
let event_nodes = nodes.get(0); let mut events: Vec<Event> = vec![];
let mut events : Vec<Event> = vec![];
parse_events(&doc, event_nodes, &mut events);
let current_event_nodes = nodes.get(0);
parse_events(&doc, current_event_nodes, &mut events);
let upcoming_event_nodes = nodes.get(1);
parse_events(&doc, upcoming_event_nodes, &mut events);
events events
} }
fn parse_events(doc: &&str, event_nodes: Option<&Node>, events: &mut Vec<Event>) { fn parse_events(doc: &&str, event_nodes: Option<&Node>, events: &mut Vec<Event>) {
if let Some(x) = event_nodes { if let Some(x) = event_nodes {
for event in x.find(Class("accordion-item")) { for event in x.find(Class("accordion-item")) {
let mut attributes = event.attr("class").expect("").split(' '); let mut attributes = event.attr("class").expect("Cant find class attribute").split(' ');
if attributes.clone().count() != 2 { continue; } if attributes.clone().count() != 2 { continue; }
let name = event.find(Class("event-name")).next().expect("Cannot find name").text(); let name = event.find(Class("event-name")).next().expect("Cannot find name").text();
let (image, color) = get_image_color(&doc, &mut attributes); let (image, color) = get_image_color(doc, &mut attributes);
let description = get_description(event); let description = get_description(event);
let time = event.find(Class("time")).next().map(|x| x.text()); let time = get_time(event);
let event_type = get_event_type(event, &description); let event_type = get_event_type(event, &description);
@ -84,16 +123,30 @@ fn parse_events(doc: &&str, event_nodes: Option<&Node>, events: &mut Vec<Event>)
}; };
} }
fn get_upcoming_events_from_document(doc: &str) -> Vec<Event> {
let document = Document::from(doc);
let nodes = document.find(Class("event-tracker")).collect::<Vec<Node>>();
let event_nodes = nodes.get(1);
let mut events : Vec<Event> = vec![];
parse_events(&doc, event_nodes, &mut events); fn get_date(date_string: &str) -> Option<String> {
events let date_regex = Regex::new(r"(?m)[0-9]{4}/(?:1[0-9]|0[1-9])/(?:0[1-9]|[1-2][0-9]|3[0-1]) (?:(?:[01][0-9]|2[0-3])|[0-9]):[0-5][0-9]").expect("Cannot compile time regex");
date_regex.captures(date_string).map(|x| x.get(0).expect("No captures found").as_str().to_string())
} }
fn get_time(event: Node) -> EventTime {
event.find(Class("duration")).next().map(|x| {
let text = x.text();
let dates = Regex::new(r" [-] ").expect("Can't create split time regex").split(text.as_str()).collect::<Vec<&str>>();
let start_date_text = dates.first().expect("Cannot get start date string").to_owned();
let end_date_text = dates.get(1).expect("Cannot get end date string").to_owned();
let start = get_date(start_date_text).map(|x| {
NaiveDateTime::parse_from_str(x.as_str(), "%Y/%m/%d %H:%M").expect("Date").timestamp()
});
let end = get_date(end_date_text).map(|x| {
NaiveDateTime::parse_from_str(x.as_str(), "%Y/%m/%d %H:%M").expect("Date").timestamp()
});
EventTime {start, end}
}).expect("No time found")
}
fn get_codes_from_document(doc: &str) -> Option<Vec<Code>> { fn get_codes_from_document(doc: &str) -> Option<Vec<Code>> {
let document = Document::from(doc); let document = Document::from(doc);
document.find(Class("codes")).next().map( |codes | { document.find(Class("codes")).next().map( |codes | {
@ -125,7 +178,7 @@ fn get_description(event: Node) -> Option<(String, String)> {
let description = event.find(Class("description")).next().map(|x| { let description = event.find(Class("description")).next().map(|x| {
let text = x.text(); let text = x.text();
let desc = text.split(": ").collect::<Vec<&str>>(); let desc = text.split(": ").collect::<Vec<&str>>();
(desc.first().expect("").to_string(), desc.get(1).expect("").to_string()) (desc.first().expect("Cannot get description title").to_string(), desc.get(1).expect("Cannot get description text").to_string())
}); });
description description
} }
@ -163,7 +216,7 @@ fn get_event_type(event: Node, description: &Option<(String, String)>) -> EventT
fn get_banner_data(event: Node) -> Option<BannerData> { fn get_banner_data(event: Node) -> Option<BannerData> {
let five_stars = event.find(Class("rarity-5").descendant(Name("picture")).descendant(Name("img"))).next().map(|x| x.attr("alt").expect("No alt on five star image").to_string()); let five_stars = event.find(Class("rarity-5").descendant(Name("picture")).descendant(Name("img"))).next().map(|x| x.attr("alt").expect("No alt on five star image").to_string());
let four_stars = event.find(Class("rarity-4")).take(3).map(|four_stars_node| { let four_stars = event.find(Class("rarity-4")).take(3).map(|four_stars_node| {
four_stars_node.find(Name("picture").descendant(Name("img"))).next().map(|x| x.attr("alt").expect("No alt on four star image").to_string()).expect("") four_stars_node.find(Name("picture").descendant(Name("img"))).next().map(|x| x.attr("alt").expect("No alt on four star image").to_string()).expect("Cannot get four stars names")
}).collect::<Vec<String>>(); }).collect::<Vec<String>>();
match five_stars { match five_stars {
@ -176,7 +229,7 @@ fn get_banner_data(event: Node) -> Option<BannerData> {
} }
fn create_current_events_embeds(doc : &str) -> Vec<CreateEmbed> { fn create_current_events_embeds(doc : &str) -> Vec<CreateEmbed> {
let mut events = get_current_events_from_document(doc); let mut events = get_current_events_from_document(doc).into_iter().take(8).collect::<Vec<Event>>();
events.sort_by(sort_events()); events.sort_by(sort_events());
@ -193,8 +246,8 @@ fn create_current_events_embeds(doc : &str) -> Vec<CreateEmbed> {
embed = embed.color(color); embed = embed.color(color);
} }
if let Some(time) = event.time { if let Some(time) = event.time.end {
embed = embed.description(format!("Time remaining : {}", time)); embed = embed.description(format!("Ends <t:{}:R>", time));
} }
if let Some(description) = event.description { if let Some(description) = event.description {
@ -217,17 +270,16 @@ fn create_upcoming_embed(doc : &str) -> Option<CreateEmbed> {
events.sort_by(sort_events()); events.sort_by(sort_events());
if events.is_empty() {return None;} if events.is_empty() {return None;}
let banner_events : Vec<Event> = events.to_vec().into_iter().filter(|p| matches!(p.event_type, EventType::ConeBanner | EventType::CharacterBanner)).collect(); let banner_events : Vec<Event> = events.iter().cloned().filter(|p| matches!(p.event_type, EventType::ConeBanner | EventType::CharacterBanner)).collect();
let other_events: Vec<Event> = events.iter().cloned().filter(|p| !matches!(p.event_type, EventType::ConeBanner | EventType::CharacterBanner)).rev().collect();
let other_events: Vec<Event> = events.iter().cloned().filter(|p| !matches!(p.event_type, EventType::ConeBanner | EventType::CharacterBanner)).collect();
Some(CreateEmbed::default() Some(CreateEmbed::default()
.title("Upcoming Events") .title("Upcoming Events")
.field("Banners", banner_events.iter().map(|banner| { .field("Banners", banner_events.iter().map(|banner| {
format!("{} - **{}** : Starts in {}", banner.name, banner.banner_data.as_ref().expect("").five_stars, banner.time.as_ref().expect("")) format!("{} - **{}** : Starts <t:{}:R>", banner.name, banner.banner_data.as_ref().expect("No Banner Data on Banner").five_stars, banner.time.start.expect("No start time for Upcomming Event"))
}).collect::<Vec<String>>().join("\n"),false) }).collect::<Vec<String>>().join("\n"),false)
.field("Events", other_events.iter().map(|event| { .field("Events", other_events.iter().map(|event| {
format!("{} : Starts in {}", event.name, event.time.as_ref().expect("")) format!("{} : Starts <t:{}:R>", event.name, event.time.start.expect("No start time for Upcomming Event"))
}).collect::<Vec<String>>().join("\n"),false) }).collect::<Vec<String>>().join("\n"),false)
.color(Color::from_rgb(rand::random(), rand::random(), rand::random())) .color(Color::from_rgb(rand::random(), rand::random(), rand::random()))
.footer(|f| f.text("Data from https://www.prydwen.gg")) .footer(|f| f.text("Data from https://www.prydwen.gg"))
@ -245,6 +297,7 @@ fn create_codes_embed(doc : &str) -> Option<CreateEmbed> {
false => format!("- {}", code.code) false => format!("- {}", code.code)
} }
}).collect::<Vec<String>>().join("\n")) }).collect::<Vec<String>>().join("\n"))
.url("https://hsr.hoyoverse.com/gift")
.color(Color::from_rgb(rand::random(), rand::random(), rand::random())) .color(Color::from_rgb(rand::random(), rand::random(), rand::random()))
.footer(|f| f.text("Data from https://www.prydwen.gg")) .footer(|f| f.text("Data from https://www.prydwen.gg"))
.to_owned()) .to_owned())
@ -253,6 +306,7 @@ fn create_codes_embed(doc : &str) -> Option<CreateEmbed> {
pub async fn create_event_embeds() -> Vec<CreateEmbed> { pub async fn create_event_embeds() -> Vec<CreateEmbed> {
let doc = get_main_prydwen().await; let doc = get_main_prydwen().await;
let mut embeds: Vec<CreateEmbed> = vec![]; let mut embeds: Vec<CreateEmbed> = vec![];
embeds.append(create_current_events_embeds(&doc).as_mut()); embeds.append(create_current_events_embeds(&doc).as_mut());
if let Some(embed) = create_upcoming_embed(&doc) { if let Some(embed) = create_upcoming_embed(&doc) {
@ -281,7 +335,17 @@ fn sort_events() -> fn(&Event, &Event) -> Ordering {
(EventType::Other, EventType::ConeBanner) => Ordering::Greater, (EventType::Other, EventType::ConeBanner) => Ordering::Greater,
(EventType::ConeBanner, EventType::CharacterBanner) => Ordering::Greater, (EventType::ConeBanner, EventType::CharacterBanner) => Ordering::Greater,
(_, _) => { Ordering::Equal } (_, _) => {
match (a.time.end, b.time.end) {
(Some(a_end), Some(b_end)) => {
match a_end > b_end {
true => Ordering::Greater,
false => Ordering::Less
}
}
_ => Ordering::Equal
}
}
} }
} }
} }
@ -294,7 +358,7 @@ pub async fn create_event_message(
#[description = "Create Event Tab"] channel: Channel #[description = "Create Event Tab"] channel: Channel
) -> Result<(), Error> { ) -> Result<(), Error> {
let message = get_discord_status_message(ctx.guild().expect("").id.0).await; let message = get_discord_status_message(ctx.guild().expect("Message not sent in guild").id.0).await;
if let Some(e) = message { if let Some(e) = message {
let rm = ChannelId::from(e.channel_id as u64) let rm = ChannelId::from(e.channel_id as u64)
.message(&ctx.http(), MessageId::from(e.message_id as u64)) .message(&ctx.http(), MessageId::from(e.message_id as u64))

View file

@ -3,13 +3,17 @@ mod data;
mod utils; mod utils;
mod mongo; mod mongo;
use std::any::Any;
use std::fmt::Display;
use std::hash::Hash;
use std::time::Duration; use std::time::Duration;
use poise::FrameworkError;
use poise::serenity_prelude::GatewayIntents; use poise::serenity_prelude::GatewayIntents;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity::model::prelude::Activity; use serenity::model::prelude::Activity;
use crate::commands::events::create_event_embeds; use crate::commands::events::create_event_embeds;
use crate::data::{Data}; use crate::data::{Data, Error};
use crate::mongo::core::get_all_status_messages; use crate::mongo::core::get_all_status_messages;
fn update_daily(ctx: Context) { fn update_daily(ctx: Context) {