1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-22 02:54:21 +00:00

Add /diario command (#11)

Co-authored-by: Lorenzo Rossi <snowycoder@gmail.com>
This commit is contained in:
Steffo 2024-08-08 00:36:39 +02:00 committed by GitHub
parent 15576aa73b
commit 437f3cf565
Signed by: github
GPG key ID: B5690EEEBB952194
17 changed files with 245 additions and 73 deletions

1
Cargo.lock generated
View file

@ -287,6 +287,7 @@ checksum = "62d6dcd069e7b5fe49a302411f759d4cf1cf2c27fe798ef46fb8baefc053dd2b"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys", "pq-sys",

View file

@ -66,7 +66,7 @@ features = ["derive"]
[dependencies.diesel] [dependencies.diesel]
version = "2.2.1" version = "2.2.1"
features = ["postgres"] features = ["postgres", "chrono"]
optional = true optional = true
[dependencies.diesel_migrations] [dependencies.diesel_migrations]
@ -108,6 +108,7 @@ default = [
interface_database = [ interface_database = [
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"chrono",
] ]
interface_stratz = [ interface_stratz = [
"graphql_client" "graphql_client"
@ -116,8 +117,8 @@ service_telegram = [
"interface_database", "interface_database",
"teloxide", "teloxide",
"rand", "rand",
"parse_datetime",
"chrono", "chrono",
"parse_datetime"
] ]
service_brooch = [ service_brooch = [
"interface_database", "interface_database",

View file

@ -2,7 +2,7 @@
# see https://diesel.rs/guides/configuring-diesel-cli # see https://diesel.rs/guides/configuring-diesel-cli
[print_schema] [print_schema]
file = "src/database/schema.rs" file = "src/interfaces/database/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory] [migrations_directory]

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS diario;

View file

@ -0,0 +1,13 @@
CREATE TABLE diario (
id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY(START 10000),
saver_id INT REFERENCES users (id) DEFAULT null,
saved_on TIMESTAMP DEFAULT now(),
quoted_id INT REFERENCES users (id) DEFAULT null,
quoted_name VARCHAR DEFAULT null,
warning TEXT DEFAULT null,
quote TEXT NOT NULL,
context TEXT DEFAULT null
);

View file

@ -1,47 +0,0 @@
// @generated automatically by Diesel CLI.
diesel::table! {
brooch_match (id) {
id -> Int8,
}
}
diesel::table! {
discord (discord_id) {
user_id -> Int4,
discord_id -> Int8,
}
}
diesel::table! {
steam (steam_id) {
user_id -> Int4,
steam_id -> Int8,
}
}
diesel::table! {
telegram (telegram_id) {
user_id -> Int4,
telegram_id -> Int8,
}
}
diesel::table! {
users (id) {
id -> Int4,
username -> Varchar,
}
}
diesel::joinable!(discord -> users (user_id));
diesel::joinable!(steam -> users (user_id));
diesel::joinable!(telegram -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
brooch_match,
discord,
steam,
telegram,
users,
);

View file

@ -1,6 +1,6 @@
use diesel::{Identifiable, Insertable, Queryable, Selectable, Associations}; use diesel::{Identifiable, Insertable, Queryable, Selectable, Associations};
use diesel::pg::Pg; use diesel::pg::Pg;
use super::schema::{users, telegram, discord, steam, brooch_match}; use super::schema::{users, telegram, discord, steam, brooch_match, diario};
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)] #[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
@ -51,3 +51,28 @@ pub struct SteamUser {
pub struct BroochMatch { pub struct BroochMatch {
pub id: i64, pub id: i64,
} }
#[derive(Debug, Clone, PartialEq, Insertable)]
#[diesel(table_name = diario)]
#[diesel(check_for_backend(Pg))]
pub struct DiarioAddition {
pub saver_id: Option<i32>,
pub warning: Option<String>,
pub quote: String,
pub quoted_name: Option<String>,
pub context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = diario)]
#[diesel(check_for_backend(Pg))]
pub struct DiarioEntry {
pub id: i32,
pub saver_id: Option<i32>,
pub saved_on: Option<chrono::NaiveDateTime>,
pub quoted_id: Option<i32>,
pub quoted_name: Option<String>,
pub warning: Option<String>,
pub quote: String,
pub context: Option<String>,
}

View file

@ -1,12 +1,24 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
#[cfg(feature = "service_brooch")]
diesel::table! { diesel::table! {
brooch_match (id) { brooch_match (id) {
id -> Int8, id -> Int8,
} }
} }
diesel::table! {
diario (id) {
id -> Int4,
saver_id -> Nullable<Int4>,
saved_on -> Nullable<Timestamp>,
quoted_id -> Nullable<Int4>,
quoted_name -> Nullable<Varchar>,
warning -> Nullable<Text>,
quote -> Text,
context -> Nullable<Text>,
}
}
diesel::table! { diesel::table! {
discord (discord_id) { discord (discord_id) {
user_id -> Int4, user_id -> Int4,
@ -41,6 +53,7 @@ diesel::joinable!(telegram -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
brooch_match, brooch_match,
diario,
discord, discord,
steam, steam,
telegram, telegram,

View file

@ -12,7 +12,7 @@ use crate::services::RoyalnetService;
use crate::utils::result::AnyResult; use crate::utils::result::AnyResult;
use crate::interfaces::stratz::{Byte, guild_matches, Long, Short}; use crate::interfaces::stratz::{Byte, guild_matches, Long, Short};
use crate::interfaces::stratz::guild_matches::{GameMode, Lane, LobbyType, Match, Player, Role, Steam}; use crate::interfaces::stratz::guild_matches::{GameMode, Lane, LobbyType, Match, Player, Role, Steam};
use crate::utils::escape::EscapableInTelegramHTML; use crate::utils::telegramdisplay::TelegramEscape;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BroochService { pub struct BroochService {

View file

@ -0,0 +1,148 @@
use std::fmt::{Error, Write};
use std::str::FromStr;
use anyhow::Context;
use once_cell::sync::Lazy;
use regex::Regex;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::prelude::Requester;
use teloxide::types::{Message, ParseMode};
use crate::interfaces::database::models::{DiarioAddition, DiarioEntry, RoyalnetUser};
use crate::services::telegram::commands::CommandResult;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::telegramdisplay::{TelegramEscape, TelegramWrite};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiarioArgs {
warning: Option<String>,
quote: String,
quoted: Option<String>,
context: Option<String>,
}
impl FromStr for DiarioArgs {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#" *(?:\[(?<warning>.+)])? *"(?<quote>.+)"[, ]*(?:[-–—]+(?<quoted>\w+)(?:, *(?<context>.+))?)?"#).unwrap());
let captures = REGEX.captures(s)
.context("Sintassi del comando incorretta.")?;
let warning = captures.name("warning")
.map(|s| s.as_str().to_owned());
let quote = captures.name("quote")
.context("Citazione non specificata nel comando.")?
.as_str()
.to_owned();
let quoted = captures.name("quoted")
.map(|s| s.as_str().to_owned());
let context = captures.name("context")
.map(|s| s.as_str().to_owned());
Ok(
Self { warning, quote, quoted, context }
)
}
}
impl TelegramWrite for DiarioEntry {
fn write_telegram<T>(&self, f: &mut T) -> Result<(), Error>
where T: Write
{
// Diario ID
write!(f, "<code>#{}</code>", self.id)?;
// Optional content warning
if let Some(warning) = self.to_owned().warning {
write!(f, ", <b>{}</b>", warning.escape_telegram_html())?;
}
// Newline
write!(f, "\n")?;
// Quote optionally covered by a spoiler tag
match self.warning.to_owned() {
None => write!(f, "<blockquote expandable>{}</blockquote>", self.clone().quote.escape_telegram_html())?,
Some(warning) => write!(f, "<blockquote expandable><tg-spoiler>{}</tg-spoiler></blockquote>", warning.escape_telegram_html())?,
}
// Newline
write!(f, "\n")?;
// Optional citation with optional context
match (self.quoted_name.to_owned(), self.context.to_owned()) {
(Some(name), Some(context)) => write!(f, "—{}, <i>{}</i>", name.escape_telegram_html(), context.escape_telegram_html())?,
(Some(name), None) => write!(f, "—{}", name.escape_telegram_html())?,
(None, Some(context)) => write!(f, "...<i>{}</i>", context.escape_telegram_html())?,
(None, None) => write!(f, "")?,
};
Ok(())
}
}
pub async fn handler(bot: &Bot, message: &Message, args: DiarioArgs, database: &DatabaseInterface) -> CommandResult {
let author = message.from()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let mut database = database.connect()?;
let royalnet_user: RoyalnetUser = {
use diesel::prelude::*;
use diesel::{ExpressionMethods, QueryDsl};
use crate::interfaces::database::schema::telegram::dsl::*;
use crate::interfaces::database::schema::users::dsl::*;
use crate::interfaces::database::models::RoyalnetUser;
telegram
.filter(telegram_id.eq::<i64>(
author.id.0.try_into()
.context("Non è stato possibile processare il tuo ID Telegram per via di un overflow.")?
))
.inner_join(users)
.select(RoyalnetUser::as_select())
.get_result(&mut database)
.context("Non è stato possibile recuperare il tuo utente Telegram dal database RYG.")?
};
let addition = DiarioAddition {
saver_id: Some(royalnet_user.id),
warning: args.warning,
quote: args.quote,
quoted_name: args.quoted,
context: args.context,
};
let entry = {
use diesel::prelude::*;
use diesel::dsl::*;
use crate::interfaces::database::schema::diario::dsl::*;
insert_into(diario)
.values(&addition)
.get_result::<DiarioEntry>(&mut database)
.context("Non è stato possibile aggiungere la riga di diario al database RYG.")?
};
let text = format!(
"🖋 Riga aggiunta al diario!\n\
\n\
{}",
entry.to_string_telegram(),
);
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.await
// teloxide does not support blockquotes yet and errors out on parsing the response
// .context("Non è stato possibile inviare la risposta.")?
;
Ok(())
}

View file

@ -20,6 +20,7 @@ mod reminder;
mod dog; mod dog;
mod cat; mod cat;
mod roll; mod roll;
mod diario;
#[derive(Debug, Clone, PartialEq, Eq, BotCommands)] #[derive(Debug, Clone, PartialEq, Eq, BotCommands)]
#[command(rename_rule = "lowercase")] #[command(rename_rule = "lowercase")]
@ -44,6 +45,8 @@ pub enum Command {
Cat, Cat,
#[command(description = "Tira un dado.")] #[command(description = "Tira un dado.")]
Roll(String), Roll(String),
#[command(description = "Salva una citazione nel diario RYG.")]
Diario(diario::DiarioArgs),
} }
impl Command { impl Command {
@ -77,6 +80,7 @@ impl Command {
Command::Dog => dog::handler(&bot, &message).await, Command::Dog => dog::handler(&bot, &message).await,
Command::Cat => cat::handler(&bot, &message).await, Command::Cat => cat::handler(&bot, &message).await,
Command::Roll(roll) => roll::handler(&bot, &message, &roll).await, Command::Roll(roll) => roll::handler(&bot, &message, &roll).await,
Command::Diario(args) => diario::handler(&bot, &message, args, &database).await,
}; };
if result.is_ok() { if result.is_ok() {
@ -118,7 +122,7 @@ async fn error_command(bot: &Bot, chat_id: ChatId, message_id: MessageId, error:
pub async fn unknown_command(bot: Bot, message: Message) -> CommandResult { pub async fn unknown_command(bot: Bot, message: Message) -> CommandResult {
log::debug!("Received an unknown command."); log::debug!("Received an unknown command.");
bot.send_message(message.chat.id, "⚠️ Comando sconosciuto.") bot.send_message(message.chat.id, "⚠️ Comando sconosciuto o sintassi non valida.")
.reply_to_message_id(message.id) .reply_to_message_id(message.id)
.await .await
.context("Non è stato possibile inviare la risposta.")?; .context("Non è stato possibile inviare la risposta.")?;

View file

@ -7,7 +7,7 @@ use teloxide::types::{Message, ParseMode};
use parse_datetime::parse_datetime_at_date; use parse_datetime::parse_datetime_at_date;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use crate::utils::escape::EscapableInTelegramHTML; use crate::utils::telegramdisplay::TelegramEscape;
use super::{CommandResult}; use super::{CommandResult};

View file

@ -5,7 +5,7 @@ use teloxide::requests::Requester;
use teloxide::types::{Message, ParseMode}; use teloxide::types::{Message, ParseMode};
use crate::interfaces::database::models::{RoyalnetUser}; use crate::interfaces::database::models::{RoyalnetUser};
use crate::services::telegram::deps::interface_database::DatabaseInterface; use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::escape::EscapableInTelegramHTML; use crate::utils::telegramdisplay::TelegramEscape;
use super::{CommandResult}; use super::{CommandResult};
pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface) -> CommandResult { pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface) -> CommandResult {

View file

@ -8,7 +8,7 @@ use teloxide::dptree::entry;
use crate::services::telegram::commands::Command; use crate::services::telegram::commands::Command;
use crate::services::telegram::deps::interface_database::DatabaseInterface; use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::result::{AnyError, AnyResult}; use crate::utils::result::{AnyError, AnyResult};
use crate::utils::escape::EscapableInTelegramHTML; use crate::utils::telegramdisplay::TelegramEscape;
use super::RoyalnetService; use super::RoyalnetService;
mod commands; mod commands;

View file

@ -1,15 +0,0 @@
pub trait EscapableInTelegramHTML {
fn escape_telegram_html(self) -> String;
}
impl<T> EscapableInTelegramHTML for T
where String: From<T>
{
fn escape_telegram_html(self) -> String {
let s: String = String::from(self);
let s = s.replace("<", "&lt;");
let s = s.replace(">", "&gt;");
let s = s.replace("&", "&amp;");
s
}
}

View file

@ -1,4 +1,4 @@
pub mod time; pub mod time;
pub mod version; pub mod version;
pub mod result; pub mod result;
pub mod escape; pub mod telegramdisplay;

View file

@ -0,0 +1,28 @@
use std::fmt::Write;
pub trait TelegramWrite {
fn write_telegram<T>(&self, f: &mut T) -> Result<(), std::fmt::Error>
where T: Write;
fn to_string_telegram(&self) -> String {
let mut result = String::new();
self.write_telegram(&mut result).unwrap();
result
}
}
pub trait TelegramEscape {
fn escape_telegram_html(self) -> String;
}
impl<T> TelegramEscape for T
where String: From<T>
{
fn escape_telegram_html(self) -> String {
let s: String = String::from(self);
let s = s.replace("<", "&lt;");
let s = s.replace(">", "&gt;");
let s = s.replace("&", "&amp;");
s
}
}