mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Implement user api from rosu
This commit is contained in:
parent
b5013b9899
commit
f5cc201f1c
7 changed files with 210 additions and 52 deletions
|
@ -187,7 +187,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<OsuClient>().unwrap();
|
||||||
|
|
||||||
let user = args.single::<String>()?;
|
let user = args.single::<String>()?;
|
||||||
let u = match osu.user(UserID::Auto(user), |f| f).await? {
|
let u = match osu.user(UserID::from_string(user), |f| f).await? {
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => {
|
None => {
|
||||||
msg.reply(&ctx, "user not found...").await?;
|
msg.reply(&ctx, "user not found...").await?;
|
||||||
|
@ -300,7 +300,9 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
|
||||||
let target = args.single::<serenity::model::id::UserId>()?;
|
let target = args.single::<serenity::model::id::UserId>()?;
|
||||||
|
|
||||||
let username = args.quoted().trimmed().single::<String>()?;
|
let username = args.quoted().trimmed().single::<String>()?;
|
||||||
let user: Option<User> = osu.user(UserID::Auto(username.clone()), |f| f).await?;
|
let user: Option<User> = osu
|
||||||
|
.user(UserID::from_string(username.clone()), |f| f)
|
||||||
|
.await?;
|
||||||
match user {
|
match user {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
add_user(target, u, &data).await?;
|
add_user(target, u, &data).await?;
|
||||||
|
@ -358,7 +360,7 @@ async fn to_user_id_query(
|
||||||
msg: &Message,
|
msg: &Message,
|
||||||
) -> Result<UserID, Error> {
|
) -> Result<UserID, Error> {
|
||||||
let id = match s {
|
let id = match s {
|
||||||
Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)),
|
Some(UsernameArg::Raw(s)) => return Ok(UserID::from_string(s)),
|
||||||
Some(UsernameArg::Tagged(r)) => r,
|
Some(UsernameArg::Tagged(r)) => r,
|
||||||
None => msg.author.id,
|
None => msg.author.id,
|
||||||
};
|
};
|
||||||
|
|
|
@ -80,9 +80,7 @@ impl Client {
|
||||||
) -> Result<Option<User>, Error> {
|
) -> Result<Option<User>, Error> {
|
||||||
let mut r = UserRequestBuilder::new(user);
|
let mut r = UserRequestBuilder::new(user);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::User> = r.build(self).await?.json().await?;
|
r.build(self).await
|
||||||
let res = vec_try_into(res)?;
|
|
||||||
Ok(res.into_iter().next())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scores(
|
pub async fn scores(
|
||||||
|
|
|
@ -13,10 +13,6 @@ pub(crate) mod rosu;
|
||||||
pub use mods::Mods;
|
pub use mods::Mods;
|
||||||
use serenity::utils::MessageBuilder;
|
use serenity::utils::MessageBuilder;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref EVENT_RANK_REGEX: Regex = Regex::new(r#"^.+achieved .*rank #(\d+).* on .+\((.+)\)$"#).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub enum ApprovalStatus {
|
pub enum ApprovalStatus {
|
||||||
Loved,
|
Loved,
|
||||||
|
@ -436,13 +432,15 @@ impl Beatmap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct UserEvent {
|
pub enum UserEvent {
|
||||||
pub display_html: String,
|
Rank(UserEventRank),
|
||||||
pub beatmap_id: Option<u64>,
|
OtherV1 {
|
||||||
pub beatmapset_id: Option<u64>,
|
display_html: String,
|
||||||
pub date: DateTime<Utc>,
|
date: DateTime<Utc>,
|
||||||
pub epic_factor: u8,
|
epic_factor: u8,
|
||||||
|
},
|
||||||
|
OtherV2(rosu_v2::model::recent_event::RecentEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a "achieved rank #x on beatmap" event.
|
/// Represents a "achieved rank #x on beatmap" event.
|
||||||
|
@ -457,19 +455,14 @@ pub struct UserEventRank {
|
||||||
impl UserEvent {
|
impl UserEvent {
|
||||||
/// Try to parse the event into a "rank" event.
|
/// Try to parse the event into a "rank" event.
|
||||||
pub fn to_event_rank(&self) -> Option<UserEventRank> {
|
pub fn to_event_rank(&self) -> Option<UserEventRank> {
|
||||||
let captures = EVENT_RANK_REGEX.captures(self.display_html.as_str())?;
|
match self {
|
||||||
let rank: u16 = captures.get(1)?.as_str().parse().ok()?;
|
UserEvent::Rank(r) => Some(r.clone()),
|
||||||
let mode: Mode = Mode::parse_from_display(captures.get(2)?.as_str())?;
|
_ => None,
|
||||||
Some(UserEventRank {
|
}
|
||||||
beatmap_id: self.beatmap_id?,
|
|
||||||
date: self.date,
|
|
||||||
mode,
|
|
||||||
rank,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
|
@ -7,12 +7,17 @@ use std::convert::TryFrom;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{error::Error, fmt, str::FromStr};
|
use std::{error::Error, fmt, str::FromStr};
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref EVENT_RANK_REGEX: Regex = Regex::new(r#"^.+achieved .*rank #(\d+).* on .+\((.+)\)$"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// Errors that can be identified from parsing.
|
/// Errors that can be identified from parsing.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
InvalidValue { field: &'static str, value: String },
|
InvalidValue { field: &'static str, value: String },
|
||||||
FromStr(String),
|
FromStr(String),
|
||||||
NoApprovalDate,
|
NoApprovalDate,
|
||||||
|
NotUserEventRank,
|
||||||
DateParseError(ChronoParseError),
|
DateParseError(ChronoParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +31,7 @@ impl fmt::Display for ParseError {
|
||||||
} => write!(f, "Invalid value `{}` for {}", value, field),
|
} => write!(f, "Invalid value `{}` for {}", value, field),
|
||||||
FromStr(ref s) => write!(f, "Invalid value `{}` parsing from string", s),
|
FromStr(ref s) => write!(f, "Invalid value `{}` parsing from string", s),
|
||||||
NoApprovalDate => write!(f, "Approval date expected but not found"),
|
NoApprovalDate => write!(f, "Approval date expected but not found"),
|
||||||
|
NotUserEventRank => write!(f, "Trying to parse user event as UserEventRank"),
|
||||||
DateParseError(ref r) => write!(f, "Error parsing date: {}", r),
|
DateParseError(ref r) => write!(f, "Error parsing date: {}", r),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,15 +159,48 @@ impl TryFrom<raw::Beatmap> for Beatmap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_user_event(s: raw::UserEvent) -> ParseResult<UserEvent> {
|
fn parse_user_event(s: raw::UserEvent) -> ParseResult<UserEvent> {
|
||||||
Ok(UserEvent {
|
match parse_user_event_rank(&s) {
|
||||||
|
Ok(r) => return Ok(UserEvent::Rank(r)),
|
||||||
|
Err(_) => (),
|
||||||
|
};
|
||||||
|
Ok(UserEvent::OtherV1 {
|
||||||
display_html: s.display_html,
|
display_html: s.display_html,
|
||||||
beatmap_id: s.beatmap_id.map(parse_from_str).transpose()?,
|
|
||||||
beatmapset_id: s.beatmapset_id.map(parse_from_str).transpose()?,
|
|
||||||
date: parse_date(&s.date)?,
|
date: parse_date(&s.date)?,
|
||||||
epic_factor: parse_from_str(&s.epicfactor)?,
|
epic_factor: parse_from_str(&s.epicfactor)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_user_event_rank(s: &raw::UserEvent) -> ParseResult<UserEventRank> {
|
||||||
|
let captures = EVENT_RANK_REGEX
|
||||||
|
.captures(s.display_html.as_str())
|
||||||
|
.ok_or(ParseError::NotUserEventRank)?;
|
||||||
|
let rank: u16 = captures
|
||||||
|
.get(1)
|
||||||
|
.ok_or(ParseError::NotUserEventRank)?
|
||||||
|
.as_str()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseError::NotUserEventRank)?;
|
||||||
|
let mode = super::Mode::parse_from_display(
|
||||||
|
captures
|
||||||
|
.get(2)
|
||||||
|
.ok_or(ParseError::NotUserEventRank)?
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.ok_or(ParseError::NotUserEventRank)?;
|
||||||
|
let beatmap_id = s
|
||||||
|
.beatmap_id
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ParseError::NotUserEventRank)?
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|_| ParseError::NotUserEventRank)?;
|
||||||
|
Ok(UserEventRank {
|
||||||
|
beatmap_id,
|
||||||
|
date: parse_date(&s.date)?,
|
||||||
|
mode,
|
||||||
|
rank,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_mode(s: impl AsRef<str>) -> ParseResult<Mode> {
|
fn parse_mode(s: impl AsRef<str>) -> ParseResult<Mode> {
|
||||||
let t: u8 = parse_from_str(s)?;
|
let t: u8 = parse_from_str(s)?;
|
||||||
use Mode::*;
|
use Mode::*;
|
||||||
|
|
|
@ -71,7 +71,6 @@ pub(crate) struct User {
|
||||||
pub(crate) struct UserEvent {
|
pub(crate) struct UserEvent {
|
||||||
pub display_html: String,
|
pub display_html: String,
|
||||||
pub beatmap_id: Option<String>,
|
pub beatmap_id: Option<String>,
|
||||||
pub beatmapset_id: Option<String>,
|
|
||||||
pub date: String,
|
pub date: String,
|
||||||
pub epicfactor: String,
|
pub epicfactor: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,69 @@ impl Beatmap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub(crate) fn from_rosu(
|
||||||
|
user: rosu::user::User,
|
||||||
|
stats: rosu::user::UserStatistics,
|
||||||
|
events: Vec<rosu::recent_event::RecentEvent>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: user.user_id as u64,
|
||||||
|
username: user.username.into_string(),
|
||||||
|
joined: time_to_utc(user.join_date),
|
||||||
|
country: user.country_code.to_string(),
|
||||||
|
count_300: 0, // why do we even want this
|
||||||
|
count_100: 0, // why do we even want this
|
||||||
|
count_50: 0, // why do we even want this
|
||||||
|
play_count: stats.playcount as u64,
|
||||||
|
played_time: Duration::from_secs(stats.playtime as u64),
|
||||||
|
ranked_score: stats.ranked_score,
|
||||||
|
total_score: stats.total_score,
|
||||||
|
count_ss: stats.grade_counts.ss as u64,
|
||||||
|
count_ssh: stats.grade_counts.ssh as u64,
|
||||||
|
count_s: stats.grade_counts.s as u64,
|
||||||
|
count_sh: stats.grade_counts.sh as u64,
|
||||||
|
count_a: stats.grade_counts.a as u64,
|
||||||
|
events: events.into_iter().map(UserEvent::from).collect(),
|
||||||
|
rank: stats.global_rank.unwrap_or(0) as u64,
|
||||||
|
country_rank: stats.country_rank.unwrap_or(0) as u64,
|
||||||
|
level: stats.level.current as f64 + stats.level.progress as f64 / 100.0,
|
||||||
|
pp: Some(stats.pp as f64),
|
||||||
|
accuracy: stats.accuracy as f64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rosu::recent_event::RecentEvent> for UserEvent {
|
||||||
|
fn from(value: rosu::recent_event::RecentEvent) -> Self {
|
||||||
|
match value.event_type {
|
||||||
|
rosu::recent_event::EventType::Rank {
|
||||||
|
grade: _,
|
||||||
|
rank,
|
||||||
|
mode,
|
||||||
|
beatmap,
|
||||||
|
user: _,
|
||||||
|
} => Self::Rank(UserEventRank {
|
||||||
|
beatmap_id: {
|
||||||
|
beatmap
|
||||||
|
.url
|
||||||
|
.trim_start_matches("/b/")
|
||||||
|
.trim_end_matches("?m=0")
|
||||||
|
.trim_end_matches("?m=1")
|
||||||
|
.trim_end_matches("?m=2")
|
||||||
|
.trim_end_matches("?m=3")
|
||||||
|
.parse::<u64>()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
rank: rank as u16,
|
||||||
|
mode: mode.into(),
|
||||||
|
date: time_to_utc(value.created_at),
|
||||||
|
}),
|
||||||
|
_ => Self::OtherV2(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Difficulty {
|
impl Difficulty {
|
||||||
pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self {
|
pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -98,6 +161,17 @@ impl From<rosu::GameMode> for Mode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Mode> for rosu::GameMode {
|
||||||
|
fn from(value: Mode) -> Self {
|
||||||
|
match value {
|
||||||
|
Mode::Std => rosu::GameMode::Osu,
|
||||||
|
Mode::Taiko => rosu::GameMode::Taiko,
|
||||||
|
Mode::Catch => rosu::GameMode::Catch,
|
||||||
|
Mode::Mania => rosu::GameMode::Mania,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<rosu::beatmap::Genre> for Genre {
|
impl From<rosu::beatmap::Genre> for Genre {
|
||||||
fn from(value: rosu::beatmap::Genre) -> Self {
|
fn from(value: rosu::beatmap::Genre) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::models::{Mode, Mods};
|
use crate::models::{Mode, Mods};
|
||||||
use crate::Client;
|
use crate::Client;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use rosu_v2::error::OsuError;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
trait ToQuery {
|
trait ToQuery {
|
||||||
|
@ -52,7 +53,25 @@ impl ToQuery for (&'static str, DateTime<Utc>) {
|
||||||
pub enum UserID {
|
pub enum UserID {
|
||||||
Username(String),
|
Username(String),
|
||||||
ID(u64),
|
ID(u64),
|
||||||
Auto(String),
|
}
|
||||||
|
|
||||||
|
impl From<UserID> for rosu_v2::prelude::UserId {
|
||||||
|
fn from(value: UserID) -> Self {
|
||||||
|
match value {
|
||||||
|
UserID::Username(s) => rosu_v2::request::UserId::Name(s.into()),
|
||||||
|
UserID::ID(id) => rosu_v2::request::UserId::Id(id as u32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserID {
|
||||||
|
pub fn from_string(s: impl Into<String>) -> UserID {
|
||||||
|
let s = s.into();
|
||||||
|
match s.parse::<u64>() {
|
||||||
|
Ok(id) => UserID::ID(id),
|
||||||
|
Err(_) => UserID::Username(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToQuery for UserID {
|
impl ToQuery for UserID {
|
||||||
|
@ -61,7 +80,6 @@ impl ToQuery for UserID {
|
||||||
match self {
|
match self {
|
||||||
Username(ref s) => vec![("u", s.clone()), ("type", "string".to_owned())],
|
Username(ref s) => vec![("u", s.clone()), ("type", "string".to_owned())],
|
||||||
ID(u) => vec![("u", u.to_string()), ("type", "id".to_owned())],
|
ID(u) => vec![("u", u.to_string()), ("type", "id".to_owned())],
|
||||||
Auto(ref s) => vec![("u", s.clone())],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +100,14 @@ impl ToQuery for BeatmapRequestKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_not_found<T>(v: Result<T, OsuError>) -> Result<Option<T>, OsuError> {
|
||||||
|
match v {
|
||||||
|
Ok(v) => Ok(Some(v)),
|
||||||
|
Err(OsuError::NotFound) => Ok(None),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod builders {
|
pub mod builders {
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
|
|
||||||
|
@ -114,19 +140,30 @@ pub mod builders {
|
||||||
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Beatmap>> {
|
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Beatmap>> {
|
||||||
Ok(match self.kind {
|
Ok(match self.kind {
|
||||||
BeatmapRequestKind::Beatmap(id) => {
|
BeatmapRequestKind::Beatmap(id) => {
|
||||||
let mut bm = client.rosu.beatmap().map_id(id as u32).await?;
|
match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? {
|
||||||
let set = bm.mapset.take().unwrap();
|
Some(mut bm) => {
|
||||||
vec![models::Beatmap::from_rosu(bm, &set)]
|
let set = bm.mapset.take().unwrap();
|
||||||
|
vec![models::Beatmap::from_rosu(bm, &set)]
|
||||||
|
}
|
||||||
|
None => vec![],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BeatmapRequestKind::Beatmapset(id) => {
|
BeatmapRequestKind::Beatmapset(id) => {
|
||||||
let mut set = client.rosu.beatmapset(id as u32).await?;
|
let mut set = match handle_not_found(client.rosu.beatmapset(id as u32).await)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(vec![]),
|
||||||
|
};
|
||||||
let bms = set.maps.take().unwrap();
|
let bms = set.maps.take().unwrap();
|
||||||
bms.into_iter()
|
bms.into_iter()
|
||||||
.map(|bm| models::Beatmap::from_rosu(bm, &set))
|
.map(|bm| models::Beatmap::from_rosu(bm, &set))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
BeatmapRequestKind::BeatmapHash(hash) => {
|
BeatmapRequestKind::BeatmapHash(hash) => {
|
||||||
let mut bm = client.rosu.beatmap().checksum(hash).await?;
|
let mut bm = match handle_not_found(client.rosu.beatmap().checksum(hash).await)?
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(vec![]),
|
||||||
|
};
|
||||||
let set = bm.mapset.take().unwrap();
|
let set = bm.mapset.take().unwrap();
|
||||||
vec![models::Beatmap::from_rosu(bm, &set)]
|
vec![models::Beatmap::from_rosu(bm, &set)]
|
||||||
}
|
}
|
||||||
|
@ -159,20 +196,36 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(&self, client: &Client) -> Result<Response> {
|
pub(crate) async fn build(self, client: &Client) -> Result<Option<models::User>> {
|
||||||
Ok(client
|
let mut r = client.rosu.user(self.user);
|
||||||
.build_request("https://osu.ppy.sh/api/get_user")
|
if let Some(mode) = self.mode {
|
||||||
.await?
|
r = r.mode(mode.into());
|
||||||
.query(&self.user.to_query())
|
}
|
||||||
.query(&self.mode.to_query())
|
let mut user = match handle_not_found(r.await)? {
|
||||||
.query(
|
Some(v) => v,
|
||||||
&self
|
None => return Ok(None),
|
||||||
.event_days
|
};
|
||||||
.map(|v| ("event_days", v.to_string()))
|
let now = time::OffsetDateTime::now_utc()
|
||||||
.to_query(),
|
- time::Duration::DAY * self.event_days.unwrap_or(31);
|
||||||
)
|
let mut events =
|
||||||
.send()
|
handle_not_found(client.rosu.recent_events(user.user_id).limit(50).await)?
|
||||||
.await?)
|
.unwrap_or(vec![]);
|
||||||
|
events.retain(|e| (now <= e.created_at));
|
||||||
|
let stats = user.statistics.take().unwrap();
|
||||||
|
Ok(Some(models::User::from_rosu(user, stats, events)))
|
||||||
|
// Ok(client
|
||||||
|
// .build_request("https://osu.ppy.sh/api/get_user")
|
||||||
|
// .await?
|
||||||
|
// .query(&self.user.to_query())
|
||||||
|
// .query(&self.mode.to_query())
|
||||||
|
// .query(
|
||||||
|
// &self
|
||||||
|
// .event_days
|
||||||
|
// .map(|v| ("event_days", v.to_string()))
|
||||||
|
// .to_query(),
|
||||||
|
// )
|
||||||
|
// .send()
|
||||||
|
// .await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue