1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-21 18:44:19 +00:00

Add /matchmaking command and refactor whole codebase (#12)

This commit is contained in:
Steffo 2024-08-18 17:11:04 +02:00 committed by GitHub
parent af3ab04604
commit 56e3baa8c0
Signed by: github
GPG key ID: B5690EEEBB952194
51 changed files with 1531 additions and 374 deletions

63
Cargo.lock generated
View file

@ -49,15 +49,16 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "aquamarine"
version = "0.1.12"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f"
checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e"
dependencies = [
"include_dir",
"itertools",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.71",
]
[[package]]
@ -888,6 +889,25 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -917,9 +937,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.9.0"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
@ -1069,12 +1089,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "nom"
version = "7.1.3"
@ -1656,6 +1670,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
dependencies = [
"serde",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.5.2"
@ -1786,21 +1810,21 @@ checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
[[package]]
name = "teloxide"
version = "0.12.2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c63345cf32a8850ebddcdd769dc2d5193d5e231262d5dada264b79da01a664da"
checksum = "5f79dd283eb21b90451c03fa7c7f83b9985130efb876b33bad89a2c208ccbc16"
dependencies = [
"aquamarine",
"bytes",
"derive_more",
"dptree",
"either",
"futures",
"log",
"mime",
"pin-project",
"serde",
"serde_json",
"serde_with_macros",
"teloxide-core",
"teloxide-macros",
"thiserror",
@ -1812,9 +1836,9 @@ dependencies = [
[[package]]
name = "teloxide-core"
version = "0.9.1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "303db260110c238e3af77bb9dff18bf7a5b5196f783059b0852aab75f91d5a16"
checksum = "9e1642a7ef10e7af63b8298c8d13c0f986d4fc646d42649ff060359607f62f69"
dependencies = [
"bitflags 1.3.2",
"bytes",
@ -1824,14 +1848,13 @@ dependencies = [
"futures",
"log",
"mime",
"never",
"once_cell",
"pin-project",
"rc-box",
"reqwest 0.11.27",
"serde",
"serde_json",
"serde_with_macros",
"serde_with",
"take_mut",
"takecell",
"thiserror",
@ -1843,9 +1866,9 @@ dependencies = [
[[package]]
name = "teloxide-macros"
version = "0.7.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1d653b093dba5e44cada57a516f572167df37b8a619443e59c8c517bb6d804"
checksum = "7e2d33d809c3e7161a9ab18bedddf98821245014f0a78fa4d2c9430b2ec018c1"
dependencies = [
"heck 0.4.1",
"proc-macro2",

View file

@ -66,7 +66,8 @@ features = ["derive"]
[dependencies.diesel]
version = "2.2.1"
features = ["postgres", "chrono"]
default-features = false
features = ["postgres", "chrono", "with-deprecated"]
optional = true
[dependencies.diesel_migrations]
@ -74,7 +75,7 @@ version = "2.2.0"
optional = true
[dependencies.teloxide]
version = "0.12.2"
version = "0.13.0"
default-features = false
features = ["native-tls", "macros"]
optional = true
@ -86,7 +87,6 @@ optional = true
[dependencies.chrono]
version = "0.4.38"
optional = true
[dependencies.parse_datetime]
version = "0.6.0"
@ -108,7 +108,6 @@ default = [
interface_database = [
"diesel",
"diesel_migrations",
"chrono",
]
interface_stratz = [
"graphql_client"
@ -118,7 +117,6 @@ service_telegram = [
"teloxide",
"rand",
"parse_datetime",
"chrono",
]
service_brooch = [
"interface_database",

View file

@ -0,0 +1,7 @@
DROP TABLE IF EXISTS matchmaking_messages_telegram;
DROP TABLE IF EXISTS matchmaking_replies;
DROP TABLE IF EXISTS matchmaking_events;
DROP TYPE IF EXISTS matchmaking_choice;

View file

@ -0,0 +1,34 @@
CREATE TYPE matchmaking_choice AS ENUM (
'yes',
'late',
'maybe',
'dontw',
'cant',
'wont'
);
CREATE TABLE matchmaking_events (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
text VARCHAR NOT NULL,
starts_at TIMESTAMP NOT NULL
);
CREATE TABLE matchmaking_replies (
matchmaking_id INTEGER REFERENCES matchmaking_events(id) NOT NULL,
user_id INTEGER REFERENCES users(id) NOT NULL,
choice matchmaking_choice NOT NULL,
late_mins INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(matchmaking_id, user_id)
);
CREATE TABLE matchmaking_messages_telegram (
matchmaking_id INTEGER REFERENCES matchmaking_events(id) NOT NULL,
telegram_chat_id BIGINT NOT NULL,
telegram_message_id INTEGER NOT NULL,
PRIMARY KEY(matchmaking_id, telegram_chat_id, telegram_message_id)
)

View file

@ -11,7 +11,6 @@ pub mod interface_database {
}
}
#[cfg(feature = "service_telegram")]
pub mod service_telegram {
use micronfig::config;
@ -40,14 +39,17 @@ pub mod brooch {
}
}
#[cfg(feature = "service_telegram")]
pub struct ChatIdConversionHack(i64);
#[cfg(feature = "service_telegram")]
impl From<i64> for ChatIdConversionHack {
fn from(value: i64) -> Self {
Self(value)
}
}
#[cfg(feature = "service_telegram")]
impl From<ChatIdConversionHack> for teloxide::types::ChatId {
fn from(value: ChatIdConversionHack) -> Self {
Self(value.0)

View file

@ -3,22 +3,26 @@ use crate::services::RoyalnetService;
pub(self) mod config;
#[derive(Debug, Clone)]
pub struct RoyalnetInstance {
#[cfg(feature = "service_telegram")]
service_telegram: crate::services::telegram::TelegramService,
#[cfg(not(feature = "service_telegram"))]
service_telegram: (),
#[cfg(feature = "service_brooch")]
service_brooch: crate::services::brooch::BroochService,
#[cfg(not(feature = "service_brooch"))]
service_brooch: (),
}
impl RoyalnetInstance {
pub async fn new() -> Self {
let service_telegram = Self::setup_telegram_service().await;
let service_brooch = Self::setup_brooch_service();
Self {
service_telegram,
service_brooch,
service_telegram: Self::setup_telegram_service().await,
service_brooch: Self::setup_brooch_service().await,
}
}
@ -50,12 +54,16 @@ impl RoyalnetInstance {
log::debug!("Automatically applying database migrations...");
log::trace!("Connecting to the database...");
let mut db = crate::interfaces::database::connect(
config::interface_database::DATABASE_URL()
).expect("Unable to connect to the database to apply migrations.");
log::trace!("Applying migrations...");
crate::interfaces::database::migrate(&mut db)
.expect("Failed to automatically apply migrations to the database.");
log::trace!("Migration successful!");
}
#[cfg(not(feature = "interface_database"))]
@ -88,12 +96,12 @@ impl RoyalnetInstance {
}
#[cfg(not(feature = "service_telegram"))]
fn get_telegram_future(service: &mut crate::services::telegram::TelegramService) -> impl Future<Output = ()> + '_ {
fn get_telegram_future(_service: &mut ()) -> impl Future<Output = ()> + '_ {
async {}
}
#[cfg(feature = "service_brooch")]
fn setup_brooch_service() -> crate::services::brooch::BroochService {
async fn setup_brooch_service() -> crate::services::brooch::BroochService {
log::debug!("Setting up Brooch service...");
crate::services::brooch::BroochService::new(
@ -109,7 +117,7 @@ impl RoyalnetInstance {
}
#[cfg(not(feature = "service_brooch"))]
fn setup_brooch_service() -> () {
async fn setup_brooch_service() -> () {
log::warn!("Brooch service is not compiled in.");
()
@ -121,7 +129,7 @@ impl RoyalnetInstance {
}
#[cfg(not(feature = "service_brooch"))]
fn get_brooch_future(service: &mut crate::services::brooch::BroochService) -> impl Future<Output = ()> + '_ {
fn get_brooch_future(_service: &mut ()) -> impl Future<Output = ()> + '_ {
async {}
}
}

View file

@ -0,0 +1,5 @@
use diesel::{Connection, ConnectionResult, PgConnection};
pub fn connect(database_url: &str) -> ConnectionResult<PgConnection> {
PgConnection::establish(database_url)
}

View file

@ -0,0 +1,59 @@
#[macro_export]
macro_rules! newtype_sql {
($visibility: vis $newtype: ident: $sqltype: path as $rusttype: path) => {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, diesel::FromSqlRow, diesel::AsExpression)]
#[diesel(sql_type = $sqltype)]
$visibility struct $newtype(pub $rusttype);
impl std::fmt::Display for $newtype {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.0)
}
}
impl From<$rusttype> for $newtype {
fn from(value: $rusttype) -> Self {
Self(value)
}
}
impl From<$newtype> for $rusttype {
fn from(value: $newtype) -> Self {
value.0
}
}
impl diesel::serialize::ToSql<$sqltype, diesel::pg::Pg> for $newtype {
fn to_sql<'a>(&'a self, out: &mut diesel::serialize::Output<'a, '_, diesel::pg::Pg>) -> diesel::serialize::Result {
use diesel::serialize::ToSql;
ToSql::<$sqltype, diesel::pg::Pg>::to_sql(&self.0, out)
}
}
impl diesel::deserialize::FromSql<$sqltype, diesel::pg::Pg> for $newtype {
fn from_sql(raw: diesel::pg::PgValue) -> diesel::deserialize::Result<Self> {
use diesel::deserialize::FromSql;
FromSql::<$sqltype, diesel::pg::Pg>::from_sql(raw)
.map(Self)
}
}
impl std::str::FromStr for $newtype {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use anyhow::Context;
Ok(
Self(
s.parse::<$rusttype>()
.context("Impossible convertire a newtype.")?
)
)
}
}
};
}

View file

@ -0,0 +1,12 @@
use anyhow::anyhow;
use diesel::migration::MigrationVersion;
use diesel::PgConnection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use crate::utils::anyhow_result::AnyResult;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub fn migrate(database: &mut PgConnection) -> AnyResult<Vec<MigrationVersion>> {
database.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow!("Failed to run pending migrations: {e:?}"))
}

View file

@ -1,19 +1,10 @@
use anyhow::anyhow;
use diesel::{Connection, ConnectionResult, PgConnection};
use diesel::migration::MigrationVersion;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use crate::utils::result::AnyResult;
pub mod schema;
pub mod models;
pub mod query_prelude;
pub fn connect(database_url: &str) -> ConnectionResult<PgConnection> {
PgConnection::establish(database_url)
}
mod migrations;
mod macros;
mod connect;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub fn migrate(database: &mut PgConnection) -> AnyResult<Vec<MigrationVersion>> {
database.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow!("Failed to run pending migrations: {e:?}"))
}
pub use connect::connect;
pub use migrations::migrate;

View file

@ -1,78 +0,0 @@
use diesel::{Identifiable, Insertable, Queryable, Selectable, Associations};
use diesel::pg::Pg;
use super::schema::{users, telegram, discord, steam, brooch_match, diario};
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(Pg))]
pub struct RoyalnetUser {
pub id: i32,
pub username: String,
}
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = telegram)]
#[diesel(primary_key(telegram_id))]
#[diesel(check_for_backend(Pg))]
pub struct TelegramUser {
pub user_id: i32,
pub telegram_id: i64,
}
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = discord)]
#[diesel(primary_key(discord_id))]
#[diesel(check_for_backend(Pg))]
pub struct DiscordUser {
pub user_id: i32,
pub discord_id: i64,
}
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = steam)]
#[diesel(primary_key(steam_id))]
#[diesel(check_for_backend(Pg))]
pub struct SteamUser {
pub user_id: i32,
pub steam_id: i64,
}
#[cfg(feature = "service_brooch")]
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = brooch_match)]
#[diesel(check_for_backend(Pg))]
pub struct BroochMatch {
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

@ -0,0 +1,48 @@
use anyhow::Context;
use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, PgConnection, Queryable, QueryDsl, RunQueryDsl, Selectable};
use diesel::deserialize::FromSql;
use diesel::pg::{Pg, PgValue};
use diesel::serialize::ToSql;
use crate::newtype_sql;
use crate::utils::anyhow_result::AnyResult;
use super::super::schema::brooch_match;
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = brooch_match)]
#[diesel(check_for_backend(Pg))]
pub struct BroochMatch {
pub id: DotaMatchId,
}
impl BroochMatch {
pub fn is_flagged(database: &mut PgConnection, match_id: DotaMatchId) -> AnyResult<bool> {
use crate::interfaces::database::query_prelude::*;
use schema::brooch_match;
log::trace!("Checking if {match_id:?} is flagged...");
Ok(
brooch_match::table
.find(match_id)
.count()
.execute(database)
.context("Impossibile determinare se la partita è marcata come processata nel database RYG.")?
.gt(&0usize)
)
}
pub fn flag(database: &mut PgConnection, match_id: DotaMatchId) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
use schema::brooch_match;
log::debug!("Flagging {match_id:?} as parsed...");
diesel::insert_into(brooch_match::table)
.values(brooch_match::id.eq(match_id))
.on_conflict_do_nothing()
.get_result::<Self>(database)
.context("Impossibile marcare la partita come processata nel database RYG.")
}
}
newtype_sql!(pub DotaMatchId: diesel::sql_types::Int8 as i64);

View file

@ -0,0 +1,32 @@
use diesel::{Identifiable, Insertable, Queryable, Selectable};
use diesel::pg::Pg;
use crate::interfaces::database::models::users::RoyalnetUserId;
use crate::newtype_sql;
use super::super::schema::diario;
#[derive(Debug, Clone, PartialEq, Insertable)]
#[diesel(table_name = diario)]
#[diesel(check_for_backend(Pg))]
pub struct DiarioAddition {
pub saver_id: Option<RoyalnetUserId>,
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 Diario {
pub id: DiarioId,
pub saver_id: Option<RoyalnetUserId>,
pub saved_on: Option<chrono::NaiveDateTime>,
pub quoted_id: Option<RoyalnetUserId>,
pub quoted_name: Option<String>,
pub warning: Option<String>,
pub quote: String,
pub context: Option<String>,
}
newtype_sql!(pub DiarioId: diesel::sql_types::Int4 as i32);

View file

@ -0,0 +1,17 @@
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable};
use diesel::pg::Pg;
use crate::newtype_sql;
use super::super::schema::discord;
use super::users::{RoyalnetUser, RoyalnetUserId};
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = discord)]
#[diesel(primary_key(discord_id))]
#[diesel(check_for_backend(Pg))]
pub struct DiscordUser {
pub user_id: RoyalnetUserId,
pub discord_id: DiscordUserId,
}
newtype_sql!(pub DiscordUserId: diesel::sql_types::Int8 as i64);

View file

@ -0,0 +1,47 @@
use std::io::Write;
use diesel::{AsExpression, FromSqlRow};
use diesel::deserialize::FromSql;
use diesel::pg::{Pg, PgValue};
use diesel::serialize::{IsNull, ToSql};
use super::super::schema::sql_types;
#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
#[diesel(sql_type = sql_types::MatchmakingChoice)]
pub enum MatchmakingChoice {
Yes,
Late,
Maybe,
DontWait,
Cant,
Wont,
}
impl ToSql<sql_types::MatchmakingChoice, Pg> for MatchmakingChoice {
fn to_sql(&self, out: &mut diesel::serialize::Output<Pg>) -> diesel::serialize::Result {
match *self {
Self::Yes => out.write_all(b"yes")?,
Self::Late => out.write_all(b"late")?,
Self::Maybe => out.write_all(b"maybe")?,
Self::DontWait => out.write_all(b"dontw")?,
Self::Cant => out.write_all(b"cant")?,
Self::Wont => out.write_all(b"wont")?,
};
Ok(IsNull::No)
}
}
impl FromSql<sql_types::MatchmakingChoice, Pg> for MatchmakingChoice {
fn from_sql(raw: PgValue) -> diesel::deserialize::Result<Self> {
match raw.as_bytes() {
b"yes" => Ok(Self::Yes),
b"late" => Ok(Self::Late),
b"maybe" => Ok(Self::Maybe),
b"dontw" => Ok(Self::DontWait),
b"cant" => Ok(Self::Cant),
b"wont" => Ok(Self::Wont),
_ => Err("Unknown MatchmakingReply".into())
}
}
}

View file

@ -0,0 +1,54 @@
use anyhow::Context;
use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, PgConnection, Queryable, QueryId, Selectable};
use diesel::deserialize::FromSql;
use diesel::pg::{Pg, PgValue};
use diesel::serialize::ToSql;
use crate::newtype_sql;
use crate::utils::anyhow_result::AnyResult;
use super::super::schema::matchmaking_events;
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = matchmaking_events)]
#[diesel(check_for_backend(Pg))]
pub struct MatchmakingEvent {
pub id: MatchmakingId,
pub text: String,
pub starts_at: chrono::NaiveDateTime,
}
impl MatchmakingEvent {
/// Create a new [MatchmakingEvent].
pub fn create(database: &mut PgConnection, text: &str, starts_at: &chrono::DateTime<chrono::Local>) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
insert_into(matchmaking_events::table)
.values(&(
matchmaking_events::text.eq(text),
matchmaking_events::starts_at.eq(starts_at.naive_utc()),
))
.get_result::<Self>(database)
.context("Non è stato possibile aggiungere il matchmaking al database RYG.")
}
/// Retrieve a [MatchmakingEvent] from the database, given its [MatchmakingId].
pub fn get(database: &mut PgConnection, matchmaking_id: MatchmakingId) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
matchmaking_events::table
.filter(matchmaking_events::id.eq(matchmaking_id.0))
.get_result::<Self>(database)
.context("Non è stato possibile recuperare il matchmaking dal database RYG.")
}
pub fn has_started(&self) -> bool {
self.starts_at.lt(&chrono::Local::now().naive_utc())
}
}
newtype_sql!(pub MatchmakingId: diesel::sql_types::Int4 as i32);
impl MatchmakingId {
pub fn callback_data(&self, data: &str) -> String {
format!("matchmaking:{}:{}", &self.0, data)
}
}

View file

@ -0,0 +1,355 @@
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable};
use diesel::pg::Pg;
use crate::interfaces::database::models::MatchmakingId;
use crate::interfaces::database::models::telegram::{TelegramChatId, TelegramMessageId};
use super::matchmaking_events::MatchmakingEvent;
use super::super::schema::matchmaking_messages_telegram;
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(MatchmakingEvent, foreign_key = matchmaking_id))]
#[diesel(table_name = matchmaking_messages_telegram)]
#[diesel(primary_key(matchmaking_id, telegram_chat_id, telegram_message_id))]
#[diesel(check_for_backend(Pg))]
pub struct MatchmakingMessageTelegram {
pub matchmaking_id: MatchmakingId,
pub telegram_chat_id: TelegramChatId,
pub telegram_message_id: TelegramMessageId,
}
#[cfg(feature = "service_telegram")]
pub(crate) mod telegram_ext {
use std::cmp::Ordering;
use std::str::FromStr;
use anyhow::Context;
use super::*;
use diesel::PgConnection;
use teloxide::payloads::SendMessageSetters;
use teloxide::payloads::EditMessageTextSetters;
use teloxide::requests::Requester;
use teloxide::types::ParseMode;
use crate::interfaces::database::models::{MatchmakingChoice, MatchmakingId, MatchmakingReply, RoyalnetUser, TelegramUser};
use crate::utils::anyhow_result::AnyResult;
use crate::utils::telegram_string::TelegramEscape;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchmakingTelegramKeyboardCallback {
Yes,
Plus5Min,
Plus15Min,
Plus60Min,
Maybe,
DontWait,
Cant,
Wont,
}
impl MatchmakingTelegramKeyboardCallback {
/// Create callback data representing the [MatchmakingTelegramKeyboardCallback] in the given [MatchmakingId].
pub fn callback_data(self, matchmaking_id: MatchmakingId) -> String {
matchmaking_id.callback_data(self.into())
}
pub fn inline_button(self, matchmaking_id: MatchmakingId, text: &str) -> teloxide::types::InlineKeyboardButton {
teloxide::types::InlineKeyboardButton::new(
text,
teloxide::types::InlineKeyboardButtonKind::CallbackData(
self.callback_data(matchmaking_id)
)
)
}
}
impl FromStr for MatchmakingTelegramKeyboardCallback {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
match s {
"yes" => Self::Yes,
"5min" => Self::Plus5Min,
"15min" => Self::Plus15Min,
"60min" => Self::Plus60Min,
"maybe" => Self::Maybe,
"dontw" => Self::DontWait,
"cant" => Self::Cant,
"wont" => Self::Wont,
x => anyhow::bail!("Unknown keyboard callback: {x:?}"),
}
)
}
}
impl From<MatchmakingTelegramKeyboardCallback> for &'static str {
fn from(value: MatchmakingTelegramKeyboardCallback) -> Self {
match value {
MatchmakingTelegramKeyboardCallback::Yes => "yes",
MatchmakingTelegramKeyboardCallback::Plus5Min => "5min",
MatchmakingTelegramKeyboardCallback::Plus15Min => "15min",
MatchmakingTelegramKeyboardCallback::Plus60Min => "60min",
MatchmakingTelegramKeyboardCallback::Maybe => "maybe",
MatchmakingTelegramKeyboardCallback::DontWait => "dontw",
MatchmakingTelegramKeyboardCallback::Cant => "cant",
MatchmakingTelegramKeyboardCallback::Wont => "wont",
}
}
}
impl MatchmakingMessageTelegram {
/// Get all the [MatchmakingMessageTelegram] for a specific [MatchmakingId].
pub fn get_all(database: &mut PgConnection, matchmaking_id: MatchmakingId) -> AnyResult<Vec<Self>> {
use diesel::prelude::*;
use crate::interfaces::database::schema::matchmaking_messages_telegram;
matchmaking_messages_telegram::table
.filter(matchmaking_messages_telegram::matchmaking_id.eq(matchmaking_id.0))
.get_results::<MatchmakingMessageTelegram>(database)
.context("La query al database RYG è fallita.")
}
fn reply_markup(matchmaking_id: MatchmakingId) -> teloxide::types::InlineKeyboardMarkup {
use MatchmakingTelegramKeyboardCallback::*;
let button_yes = Yes.inline_button(matchmaking_id, "🔵 Ci sarò!");
let button_5min = Plus5Min.inline_button(matchmaking_id, "🕐 +5 min");
let button_15min = Plus15Min.inline_button(matchmaking_id, "🕒 +15 min");
let button_60min = Plus60Min.inline_button(matchmaking_id, "🕛 +60 min");
let button_maybe = Maybe.inline_button(matchmaking_id, "❔ Forse...");
let button_dontw = DontWait.inline_button(matchmaking_id, "❓ Non aspettatemi.");
let button_cant = Cant.inline_button(matchmaking_id, "🔺 Non posso...");
let button_wont = Wont.inline_button(matchmaking_id, "🔻 Non mi interessa.");
teloxide::types::InlineKeyboardMarkup::new(vec![
vec![button_yes],
vec![button_5min, button_15min, button_60min],
vec![button_maybe, button_dontw],
vec![button_cant, button_wont],
])
}
fn text(event: &MatchmakingEvent, replies: &Vec<(MatchmakingReply, RoyalnetUser, TelegramUser)>) -> String {
use std::fmt::Write;
let mut result = String::new();
let emoji = match event.has_started() {
false => "🚩",
true => "🔔",
};
let text = event.text.as_str().escape_telegram_html();
writeln!(result, "{emoji} <b>{text}</b>").unwrap();
let start = event.starts_at.format("%c").to_string().escape_telegram_html();
writeln!(result, "<i>{start}</i>").unwrap();
writeln!(result).unwrap();
for (reply, royalnet, telegram) in replies {
use MatchmakingChoice::*;
let emoji = match reply.choice {
Yes => "🔵",
Late => match reply.late_mins {
i32::MIN..=5 => "🕐",
6..=10 => "🕑",
11..=15 => "🕒",
16..=20 => "🕓",
21..=25 => "🕔",
26..=30 => "🕕",
31..=35 => "🕖",
36..=40 => "🕗",
41..=45 => "🕘",
46..=50 => "🕙",
51..=55 => "🕚",
56..=i32::MAX => "🕛",
},
Maybe => "",
DontWait => "",
Cant => "🔺",
Wont => "🔻",
};
let telegram_id = telegram.telegram_id.0;
let username = &royalnet.username;
write!(result, "{emoji} <a href=\"tg://user?id={telegram_id}\">{username}</a>").unwrap();
if reply.choice == Late {
let late_mins = reply.late_mins;
write!(result, " (+{late_mins} mins)").unwrap();
}
writeln!(result).unwrap();
}
result
}
async fn send_new(
database: &mut PgConnection,
matchmaking_id: MatchmakingId,
bot: &teloxide::Bot,
chat_id: teloxide::types::ChatId,
reply_to: Option<teloxide::types::MessageId>,
) -> AnyResult<teloxide::types::Message> {
let event = MatchmakingEvent::get(database, matchmaking_id)
.context("Non è stato possibile recuperare il matchmaking dal database RYG.")?;
let replies = MatchmakingReply::get_all_telegram(database, matchmaking_id)
.context("Non è stato possibile recuperare le risposte al matchmaking dal database RYG.")?;
let text = Self::text(&event, &replies);
let mut request = bot.send_message(chat_id, text)
.parse_mode(ParseMode::Html)
.reply_markup(
Self::reply_markup(matchmaking_id)
);
if let Some(reply_to) = reply_to {
request = request.reply_parameters(
teloxide::types::ReplyParameters::new(reply_to)
);
}
request
.await
.context("La richiesta di invio messaggio alla Bot API di Telegram è fallita.")
}
fn create(
database: &mut PgConnection,
matchmaking_id: MatchmakingId,
reply: &teloxide::types::Message,
)
-> AnyResult<Self>
{
use diesel::prelude::*;
use diesel::dsl::*;
use crate::interfaces::database::schema::matchmaking_messages_telegram;
insert_into(matchmaking_messages_telegram::table)
.values(&MatchmakingMessageTelegram {
matchmaking_id,
telegram_chat_id: reply.chat.id.into(),
telegram_message_id: reply.id.into(),
})
.on_conflict_do_nothing()
.get_result::<MatchmakingMessageTelegram>(database)
.context("L'inserimento nel database RYG è fallito.")
}
pub async fn send_new_and_create(
database: &mut PgConnection,
matchmaking_id: MatchmakingId,
bot: &teloxide::Bot,
chat_id: teloxide::types::ChatId,
reply_to: Option<teloxide::types::MessageId>,
)
-> AnyResult<Self>
{
let reply = Self::send_new(database, matchmaking_id, bot, chat_id, reply_to)
.await
.context("Non è stato possibile inviare il messaggio Telegram del matchmaking.")?;
let this = Self::create(database, matchmaking_id, &reply)
.context("Non è stato possibile aggiungere il messaggio Telegram al database RYG.")?;
Ok(this)
}
async fn send_edit(
&self,
bot: &teloxide::Bot,
text: &str,
with_keyboard: bool,
)
-> AnyResult<teloxide::types::Message>
{
let telegram_chat_id: teloxide::types::ChatId = self.telegram_chat_id.into();
let mut request = bot.edit_message_text(telegram_chat_id, self.telegram_message_id.into(), text)
.parse_mode(ParseMode::Html);
if with_keyboard {
request = request.reply_markup(
Self::reply_markup(self.matchmaking_id)
)
}
request
.await
.context("La richiesta di modifica messaggio alla Bot API di Telegram è fallita.")
}
pub async fn make_text_and_send_edit(
&self,
database: &mut PgConnection,
bot: &teloxide::Bot,
)
-> AnyResult<()>
{
let event = MatchmakingEvent::get(database, self.matchmaking_id)
.context("Non è stato possibile recuperare il matchmaking dal database RYG.")?;
let replies = MatchmakingReply::get_all_telegram(database, self.matchmaking_id)
.context("Non è stato possibile recuperare le risposte al matchmaking dal database RYG.")?;
let text = Self::text(&event, &replies);
self.send_edit(bot, &text, !event.has_started())
.await
.context("Non è stato possibile modificare il messaggio Telegram del matchmaking.")?;
Ok(())
}
async fn send_delete(
&self,
bot: &teloxide::Bot,
)
-> AnyResult<teloxide::types::True>
{
bot
.delete_message::<teloxide::types::ChatId>(self.telegram_chat_id.into(), self.telegram_message_id.into())
.await
.context("La richiesta di eliminazione messaggio alla Bot API di Telegram è fallita.")
}
fn destroy(
&self,
database: &mut PgConnection,
)
-> AnyResult<usize>
{
use diesel::prelude::*;
use diesel::dsl::*;
use crate::interfaces::database::schema::matchmaking_messages_telegram;
delete(matchmaking_messages_telegram::table)
.filter(matchmaking_messages_telegram::matchmaking_id.eq(self.matchmaking_id))
.filter(matchmaking_messages_telegram::telegram_chat_id.eq(self.telegram_chat_id))
.filter(matchmaking_messages_telegram::telegram_message_id.eq(self.telegram_message_id))
.execute(database)
.context("La rimozione dal database RYG è fallita.")
}
pub async fn destroy_and_send_delete(
self,
database: &mut PgConnection,
bot: &teloxide::Bot
)
-> AnyResult<()>
{
self.destroy(database)
.context("Non è stato possibile eliminare il messaggio Telegram dal database RYG.")?;
self.send_delete(bot)
.await
.context("Non è stato possibile eliminare il messaggio Telegram del matchmaking.")?;
Ok(())
}
}
}

View file

@ -0,0 +1,79 @@
use std::ops::Add;
use anyhow::Context;
use diesel::{Associations, Identifiable, Insertable, PgConnection, Queryable, Selectable};
use diesel::pg::Pg;
use crate::interfaces::database::models::{MatchmakingId, RoyalnetUserId, TelegramUser};
use crate::utils::anyhow_result::AnyResult;
use super::matchmaking_choice::MatchmakingChoice;
use super::matchmaking_events::MatchmakingEvent;
use super::super::schema::matchmaking_replies;
use super::users::RoyalnetUser;
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(MatchmakingEvent, foreign_key = matchmaking_id))]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = matchmaking_replies)]
#[diesel(primary_key(matchmaking_id, user_id))]
#[diesel(check_for_backend(Pg))]
pub struct MatchmakingReply {
pub matchmaking_id: MatchmakingId,
pub user_id: RoyalnetUserId,
pub choice: MatchmakingChoice,
pub late_mins: i32,
}
impl MatchmakingReply {
pub fn get_all_telegram(database: &mut PgConnection, matchmaking_id: MatchmakingId) -> AnyResult<Vec<(Self, RoyalnetUser, TelegramUser)>> {
use crate::interfaces::database::query_prelude::*;
use schema::{matchmaking_replies, users, telegram};
matchmaking_replies::table
.filter(matchmaking_replies::matchmaking_id.eq(matchmaking_id))
.inner_join(users::table.on(matchmaking_replies::user_id.eq(users::id)))
.inner_join(telegram::table.on(users::id.eq(telegram::user_id)))
.get_results::<(Self, RoyalnetUser, TelegramUser)>(database)
.context("Non è stato possibile recuperare le risposte al matchmaking dal database RYG.")
}
pub fn set(database: &mut PgConnection, matchmaking_id: MatchmakingId, user_id: RoyalnetUserId, choice: MatchmakingChoice) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
use schema::matchmaking_replies;
insert_into(matchmaking_replies::table)
.values(&Self {
matchmaking_id,
user_id,
choice,
late_mins: 0,
})
.on_conflict(on_constraint("matchmaking_replies_pkey"))
.do_update()
.set((
matchmaking_replies::choice.eq(choice),
matchmaking_replies::late_mins.eq(0),
))
.get_result::<Self>(database)
.context("Non è stato possibile inserire la risposta al matchmaking nel database RYG.")
}
pub fn add_late_minutes(database: &mut PgConnection, matchmaking_id: MatchmakingId, user_id: RoyalnetUserId, increase_by: i32) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
use schema::matchmaking_replies;
insert_into(matchmaking_replies::table)
.values(&Self {
matchmaking_id,
user_id,
choice: MatchmakingChoice::Late,
late_mins: increase_by,
})
.on_conflict(on_constraint("matchmaking_replies_pkey"))
.do_update()
.set((
matchmaking_replies::choice.eq(MatchmakingChoice::Late),
matchmaking_replies::late_mins.eq(matchmaking_replies::late_mins.add(increase_by)),
))
.get_result::<Self>(database)
.context("Non è stato possibile aumentare il ritardo nella risposta nel database RYG.")
}
}

View file

@ -0,0 +1,28 @@
#![allow(unused_imports)]
use std::io::Write;
use diesel::deserialize::FromSql;
use diesel::serialize::ToSql;
mod users;
mod telegram;
mod discord;
mod steam;
mod brooch_match;
mod diario;
mod matchmaking_events;
mod matchmaking_replies;
mod matchmaking_messages_telegram;
mod matchmaking_choice;
pub use users::{RoyalnetUser, RoyalnetUserId};
pub use telegram::{TelegramUser, TelegramChatId, TelegramMessageId, TelegramUserId};
pub use discord::{DiscordUser, DiscordUserId};
pub use steam::{SteamUser, SteamId64};
pub use brooch_match::{BroochMatch, DotaMatchId};
pub use diario::{Diario, DiarioId};
pub use matchmaking_events::{MatchmakingEvent, MatchmakingId};
pub use matchmaking_replies::MatchmakingReply;
pub use matchmaking_messages_telegram::{MatchmakingMessageTelegram, telegram_ext::MatchmakingTelegramKeyboardCallback};
pub use matchmaking_choice::MatchmakingChoice;

View file

@ -0,0 +1,17 @@
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable};
use diesel::pg::Pg;
use crate::newtype_sql;
use super::super::schema::steam;
use super::users::{RoyalnetUser, RoyalnetUserId};
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = steam)]
#[diesel(primary_key(steam_id))]
#[diesel(check_for_backend(Pg))]
pub struct SteamUser {
pub user_id: RoyalnetUserId,
pub steam_id: SteamId64,
}
newtype_sql!(pub SteamId64: diesel::sql_types::Int8 as i64);

View file

@ -0,0 +1,62 @@
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable};
use diesel::pg::Pg;
use crate::newtype_sql;
use super::super::schema::telegram;
use super::users::{RoyalnetUser, RoyalnetUserId};
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(RoyalnetUser, foreign_key = user_id))]
#[diesel(table_name = telegram)]
#[diesel(primary_key(telegram_id))]
#[diesel(check_for_backend(Pg))]
pub struct TelegramUser {
pub user_id: RoyalnetUserId,
pub telegram_id: TelegramUserId,
}
newtype_sql!(pub TelegramUserId: diesel::sql_types::Int8 as i64);
newtype_sql!(pub TelegramChatId: diesel::sql_types::Int8 as i64);
newtype_sql!(pub TelegramMessageId: diesel::sql_types::Int4 as i32);
#[cfg(feature = "service_telegram")]
mod telegram_ext {
use super::*;
impl From<teloxide::types::ChatId> for TelegramChatId {
fn from(value: teloxide::types::ChatId) -> Self {
Self(value.0)
}
}
impl From<TelegramChatId> for teloxide::types::ChatId {
fn from(value: TelegramChatId) -> Self {
Self(value.0)
}
}
impl From<teloxide::types::UserId> for TelegramUserId {
fn from(value: teloxide::types::UserId) -> Self {
// FIXME: this surely seems like a great idea
Self(value.0 as i64)
}
}
impl From<TelegramUserId> for teloxide::types::UserId {
fn from(value: TelegramUserId) -> Self {
// FIXME: this surely seems like a great idea
Self(value.0 as u64)
}
}
impl From<teloxide::types::MessageId> for TelegramMessageId {
fn from(value: teloxide::types::MessageId) -> Self {
Self(value.0)
}
}
impl From<TelegramMessageId> for teloxide::types::MessageId {
fn from(value: TelegramMessageId) -> Self {
Self(value.0)
}
}
}

View file

@ -0,0 +1,16 @@
use diesel::{Identifiable, Insertable, Queryable, Selectable};
use diesel::deserialize::FromSql;
use diesel::pg::Pg;
use diesel::serialize::ToSql;
use crate::newtype_sql;
use super::super::schema::users;
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(Pg))]
pub struct RoyalnetUser {
pub id: RoyalnetUserId,
pub username: String,
}
newtype_sql!(pub RoyalnetUserId: diesel::sql_types::Int4 as i32);

View file

@ -0,0 +1,10 @@
#![allow(unused_imports)]
pub use diesel::dsl::*;
pub use diesel::query_dsl::*;
pub use diesel::upsert::*;
pub use diesel::OptionalExtension;
pub use diesel::ExpressionMethods;
pub use diesel::SelectableHelper;
pub use super::schema;
pub use super::models;

View file

@ -1,5 +1,11 @@
// @generated automatically by Diesel CLI.
pub mod sql_types {
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "matchmaking_choice"))]
pub struct MatchmakingChoice;
}
diesel::table! {
brooch_match (id) {
id -> Int8,
@ -26,6 +32,34 @@ diesel::table! {
}
}
diesel::table! {
matchmaking_events (id) {
id -> Int4,
text -> Varchar,
starts_at -> Timestamp,
}
}
diesel::table! {
matchmaking_messages_telegram (matchmaking_id, telegram_chat_id, telegram_message_id) {
matchmaking_id -> Int4,
telegram_chat_id -> Int8,
telegram_message_id -> Int4,
}
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::MatchmakingChoice;
matchmaking_replies (matchmaking_id, user_id) {
matchmaking_id -> Int4,
user_id -> Int4,
choice -> MatchmakingChoice,
late_mins -> Int4,
}
}
diesel::table! {
steam (steam_id) {
user_id -> Int4,
@ -48,6 +82,9 @@ diesel::table! {
}
diesel::joinable!(discord -> users (user_id));
diesel::joinable!(matchmaking_messages_telegram -> matchmaking_events (matchmaking_id));
diesel::joinable!(matchmaking_replies -> matchmaking_events (matchmaking_id));
diesel::joinable!(matchmaking_replies -> users (user_id));
diesel::joinable!(steam -> users (user_id));
diesel::joinable!(telegram -> users (user_id));
@ -55,6 +92,9 @@ diesel::allow_tables_to_appear_in_same_query!(
brooch_match,
diario,
discord,
matchmaking_events,
matchmaking_messages_telegram,
matchmaking_replies,
steam,
telegram,
users,

View file

@ -14,6 +14,7 @@ async fn main() {
// Create instance
let instance = RoyalnetInstance::new().await;
log::trace!("Starting {instance:?}!");
instance.run().await;
log::error!("No services configured.");

View file

@ -1,18 +1,21 @@
use std::cmp::PartialEq;
use std::time::Duration;
use anyhow::Context;
use chrono::{DateTime, TimeDelta, TimeZone, Utc};
use diesel::PgConnection;
use chrono::{DateTime, Local, TimeDelta, TimeZone};
use diesel::{PgConnection};
use reqwest::Url;
use teloxide::prelude::*;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{ChatId, LinkPreviewOptions, Message};
use tokio::time::sleep;
use crate::interfaces::database;
use crate::interfaces::database::models::{BroochMatch};
use crate::interfaces::database::models::{BroochMatch, DotaMatchId, TelegramUserId};
use crate::services::RoyalnetService;
use crate::utils::result::AnyResult;
use crate::interfaces::stratz::{Byte, guild_matches, Long, Short};
use crate::utils::anyhow_result::AnyResult;
use crate::interfaces::stratz::{Byte, guild_matches, Short};
use crate::interfaces::stratz::guild_matches::{GameMode, Lane, LobbyType, Match, Player, Role, Steam};
use crate::utils::telegramdisplay::TelegramEscape;
use crate::utils::telegram_string::TelegramEscape;
#[derive(Debug, Clone)]
pub struct BroochService {
@ -27,7 +30,18 @@ pub struct BroochService {
impl BroochService {
#[allow(clippy::too_many_arguments)]
pub fn new(database_url: String, graphql_base_url: &str, stratz_token: &str, watched_guild_id: i64, min_players_to_process: usize, telegram_bot_token: String, notification_chat_id: ChatId, max_imp_wait: TimeDelta) -> AnyResult<Self> {
pub fn new(
database_url: String,
graphql_base_url: &str,
stratz_token: &str,
watched_guild_id: i64,
min_players_to_process: usize,
telegram_bot_token: String,
notification_chat_id: ChatId,
max_imp_wait: TimeDelta
)
-> AnyResult<Self>
{
log::info!("Initializing a new Brooch service...");
let mut graphql_url = Url::parse(graphql_base_url)
@ -134,58 +148,54 @@ impl BroochService {
Ok(matches)
}
fn get_match_id(&self, r#match: &Match) -> AnyResult<Long> {
fn get_match_id(&self, r#match: &Match) -> AnyResult<DotaMatchId> {
log::trace!("Getting match id...");
r#match.id
.context("La richiesta è riuscita, ma non è stato ricevuto da STRATZ l'ID della partita.")
}
fn get_database_match(&self, database: &mut PgConnection, match_id: Long) -> AnyResult<Option<BroochMatch>> {
log::trace!("Getting match {match_id} from the database...");
let match_royalnet = {
use diesel::prelude::*;
use diesel::{ExpressionMethods, QueryDsl};
use crate::interfaces::database::schema::brooch_match::dsl::*;
brooch_match
.filter(id.eq(match_id))
.select(BroochMatch::as_select())
.get_result(database)
.optional()
.context("Non è stato possibile recuperare la partita restituita da STRATZ dal database RYG.")?
};
Ok(match_royalnet)
}
fn should_process_match_exists(&self, database: &mut PgConnection, match_id: Long) -> AnyResult<bool> {
log::trace!("Determining whether {match_id} should be processed...");
Ok(
self.get_database_match(database, match_id)?
.is_none()
r#match.id
.context("La richiesta è riuscita, ma non è stato ricevuto da STRATZ l'ID della partita.")?
.into()
)
}
fn get_match_datetime(&self, r#match: &Match) -> AnyResult<DateTime<Utc>> {
fn get_database_match(&self, database: &mut PgConnection, match_id: DotaMatchId) -> AnyResult<Option<BroochMatch>> {
use crate::interfaces::database::query_prelude::*;
use crate::interfaces::database::schema::brooch_match;
log::trace!("Getting {match_id:?} from the database...");
brooch_match::table
.filter(brooch_match::id.eq(match_id))
.get_result::<BroochMatch>(database)
.optional()
.context("Non è stato possibile recuperare la partita restituita da STRATZ dal database RYG.")
}
fn should_process_match_exists(&self, database: &mut PgConnection, match_id: DotaMatchId) -> AnyResult<bool> {
log::trace!("Determining whether {match_id:?} should be processed...");
self.get_database_match(database, match_id)
.map(|m| m.is_none())
.context("Non è stato possibile determinare se la partita restituita da STRATZ fosse stata già processata.")
}
fn get_match_datetime(&self, r#match: &Match) -> AnyResult<DateTime<Local>> {
log::trace!("Getting match datetime...");
let match_date = r#match.end_date_time
.context("Non è stato ricevuto da STRATZ il momento di termine della partita.")?;
log::trace!("Converting match datetime to DateTime<Utc> object...");
log::trace!("Converting match datetime to local datetime...");
Utc.timestamp_opt(match_date, 0)
Local.timestamp_opt(match_date, 0)
.earliest()
.context("È stato ricevuto da STRATZ un momento di termine della partita non valido.")
}
fn get_match_timedelta(&self, datetime: &DateTime<Utc>) -> TimeDelta {
fn get_match_timedelta(&self, datetime: &DateTime<Local>) -> TimeDelta {
log::trace!("Getting current time...");
let now = Utc::now();
let now = Local::now();
log::trace!("Getting match timedelta...");
@ -421,7 +431,7 @@ impl BroochService {
.context("Non è stato ricevuto da STRATZ il display name di almeno uno dei giocatori della partita.")
}
fn get_player_telegram_id(&self, database: &mut PgConnection, player_steam: Steam) -> AnyResult<Option<i64>> {
fn get_player_telegram_id(&self, database: &mut PgConnection, player_steam: Steam) -> AnyResult<Option<TelegramUserId>> {
use diesel::prelude::*;
use diesel::{ExpressionMethods, QueryDsl};
use crate::interfaces::database::schema::{steam, users, telegram};
@ -461,7 +471,7 @@ impl BroochService {
steam::steam_id.eq(player_steam_id_y1)
)
.select(TelegramUser::as_select())
.get_result(database)
.get_result::<TelegramUser>(database)
.optional()
.context("Non è stato possibile connettersi al database RYG.")?
.map(|t| t.telegram_id)
@ -608,9 +618,10 @@ impl BroochService {
hero_name.escape_telegram_html(),
)),
Some(telegram_id) => lines.push(format!(
"<u><a href=\"tg://user?id={telegram_id}\"><b>{}</b></a> ({})</u>",
name.escape_telegram_html(),
hero_name.escape_telegram_html(),
"<u><a href=\"tg://user?id={}\"><b>{}</b></a> ({})</u>",
telegram_id.to_string().escape_telegram_html(),
name.to_string().escape_telegram_html(),
hero_name.to_string().escape_telegram_html(),
)),
}
@ -641,7 +652,7 @@ impl BroochService {
Ok(lines.join("\n"))
}
fn stringify_match(&self, database: &mut PgConnection, r#match: Match) -> AnyResult<(Long, Option<String>)> {
fn stringify_match(&self, database: &mut PgConnection, r#match: Match) -> AnyResult<(DotaMatchId, Option<String>)> {
log::debug!("Stringifying match...");
let match_id = self.get_match_id(&r#match)?;
@ -699,39 +710,30 @@ impl BroochService {
}
lines.push(format!(
"Partita <code>{match_id}</code> · <a href=\"https://stratz.com/matches/{match_id}\">Apri su STRATZ</a>"
"Partita <code>{}</code>",
match_id,
));
Ok((match_id, Some(lines.join("\n"))))
}
async fn send_notification(&self, text: &str) -> AnyResult<Message> {
async fn send_notification(&self, match_id: DotaMatchId, text: &str) -> AnyResult<Message> {
log::debug!("Sending notification...");
self.telegram_bot.send_message(self.notification_chat_id, text)
.parse_mode(teloxide::types::ParseMode::Html)
.disable_notification(true)
.disable_web_page_preview(true)
.link_preview_options(LinkPreviewOptions {
is_disabled: false,
url: Some(format!("https://stratz.com/matches/{}", match_id)),
prefer_small_media: true,
prefer_large_media: false,
show_above_text: false,
})
.await
.context("Impossibile inviare la notifica di una partita.")
}
fn flag_match_id(&self, database: &mut PgConnection, match_id: Long) -> AnyResult<BroochMatch> {
use diesel::prelude::*;
use crate::interfaces::database::schema::brooch_match::dsl::*;
use crate::interfaces::database::models::{BroochMatch};
log::debug!("Flagging as parsed match id: {match_id}");
let match_royalnet = BroochMatch { id: match_id };
diesel::insert_into(brooch_match)
.values(&match_royalnet)
.returning(BroochMatch::as_returning())
.get_result(database)
.context("Impossibile marcare la partita come processata nel database RYG.")
}
async fn iteration(&self) -> AnyResult<()> {
log::debug!("Now running an iteration of brooch!");
@ -746,14 +748,14 @@ impl BroochService {
let results = matches
.into_iter()
.map(|r#match| self.stringify_match(&mut database, r#match))
.collect::<Vec<AnyResult<(Long, Option<String>)>>>();
.collect::<Vec<AnyResult<(DotaMatchId, Option<String>)>>>();
for result in results {
let (match_id, message) = result?;
if let Some(message) = message {
self.send_notification(&message).await?;
self.flag_match_id(&mut database, match_id)?;
self.send_notification(match_id, &message).await?;
BroochMatch::flag(&mut database, match_id)?;
}
}

View file

@ -1,6 +1,6 @@
use std::time::Duration;
use tokio::time::sleep;
use crate::utils::result::AnyResult;
use crate::utils::anyhow_result::AnyResult;
pub trait RoyalnetService {
async fn run(&mut self) -> AnyResult<()>;

View file

@ -4,6 +4,7 @@ use rand::seq::SliceRandom;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::prelude::{Message, Requester};
use teloxide::types::ReplyParameters;
use crate::services::telegram::commands::{CommandResult};
// Cerchiamo di tenere bilanciate le tre colonne, o almeno le prime due.
@ -84,7 +85,7 @@ pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_message(message.chat.id, answer.to_string())
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -3,7 +3,7 @@ use reqwest::Url;
use teloxide::Bot;
use teloxide::payloads::SendPhotoSetters;
use teloxide::requests::Requester;
use teloxide::types::{InputFile, Message};
use teloxide::types::{InputFile, Message, ReplyParameters};
use serde::Deserialize;
use super::{CommandResult};
@ -34,7 +34,7 @@ pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_photo(message.chat.id, input)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare un gatto in risposta a questo messaggio.")?;

View file

@ -6,11 +6,12 @@ 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 teloxide::types::{Message, ParseMode, ReplyParameters};
use crate::interfaces::database::models::Diario;
use crate::interfaces::database::models::RoyalnetUser;
use crate::services::telegram::commands::CommandResult;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::telegramdisplay::{TelegramEscape, TelegramWrite};
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
use crate::utils::telegram_string::{TelegramEscape, TelegramWrite};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiarioArgs {
@ -60,7 +61,7 @@ impl FromStr for DiarioArgs {
}
}
impl TelegramWrite for DiarioEntry {
impl TelegramWrite for Diario {
fn write_telegram<T>(&self, f: &mut T) -> Result<(), Error>
where T: Write
{
@ -96,8 +97,8 @@ impl TelegramWrite for DiarioEntry {
}
}
pub async fn handler(bot: &Bot, message: &Message, args: DiarioArgs, database: &DatabaseInterface) -> CommandResult {
let author = message.from()
pub async fn handler(bot: &Bot, message: &Message, args: &DiarioArgs, database: &DatabaseInterface) -> CommandResult {
let author = message.from.as_ref()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let mut database = database.connect()?;
@ -120,22 +121,19 @@ pub async fn handler(bot: &Bot, message: &Message, args: DiarioArgs, 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::*;
use crate::interfaces::database::query_prelude::*;
use schema::diario;
insert_into(diario)
.values(&addition)
.get_result::<DiarioEntry>(&mut database)
insert_into(diario::table)
.values(&(
diario::saver_id.eq(Some(royalnet_user.id)),
diario::warning.eq(args.warning.clone()),
diario::quote.eq(args.quote.clone()),
diario::quoted_name.eq(args.quoted.clone()),
diario::context.eq(args.context.clone()),
))
.get_result::<Diario>(&mut database)
.context("Non è stato possibile aggiungere la riga di diario al database RYG.")?
};
@ -149,7 +147,7 @@ pub async fn handler(bot: &Bot, message: &Message, args: DiarioArgs, database: &
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
// teloxide does not support blockquotes yet and errors out on parsing the response
// .context("Non è stato possibile inviare la risposta.")?

View file

@ -3,7 +3,7 @@ use reqwest::Url;
use teloxide::Bot;
use teloxide::payloads::SendPhotoSetters;
use teloxide::requests::Requester;
use teloxide::types::{InputFile, Message};
use teloxide::types::{InputFile, Message, ReplyParameters};
use serde::Deserialize;
use super::{CommandResult};
@ -30,7 +30,7 @@ pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_photo(message.chat.id, input)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare un cane in risposta a questo messaggio.")?;

View file

@ -2,7 +2,7 @@ use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Message};
use teloxide::types::{Message, ReplyParameters};
use super::{CommandResult};
pub async fn handler(bot: &Bot, message: &Message, text: &str) -> CommandResult {
@ -12,7 +12,7 @@ pub async fn handler(bot: &Bot, message: &Message, text: &str) -> CommandResult
let _reply = bot
.send_message(message.chat.id, text)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -6,6 +6,7 @@ use rand::seq::SliceRandom;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::prelude::{Message, Requester};
use teloxide::types::ReplyParameters;
use crate::services::telegram::commands::{CommandResult};
// Tutte le fortune devono essere positive, o almeno neutrali, per poter essere aggiunte.
@ -194,7 +195,7 @@ impl Hash for FortuneKey {
pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let today = chrono::Local::now().date_naive();
let author = message.from()
let author = message.from.as_ref()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let author_id = author.id;
@ -221,7 +222,7 @@ pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_message(message.chat.id, fortune.to_string())
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -2,7 +2,7 @@ use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{BotCommand, Message, ParseMode};
use teloxide::types::{BotCommand, Message, ParseMode, ReplyParameters};
use teloxide::utils::command::BotCommands;
use super::{CommandResult};
@ -14,7 +14,7 @@ pub async fn handler_all(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;
@ -68,7 +68,7 @@ pub async fn handler_specific(bot: &Bot, message: &Message, target: &str) -> Com
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -0,0 +1,69 @@
use std::str::FromStr;
use anyhow::Context;
use once_cell::sync::Lazy;
use parse_datetime::parse_datetime_at_date;
use regex::Regex;
use teloxide::Bot;
use teloxide::prelude::Message;
use crate::interfaces::database::models::MatchmakingEvent;
use crate::interfaces::database::models::MatchmakingMessageTelegram;
use crate::services::telegram::commands::CommandResult;
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
use crate::utils::time::sleep_chrono;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchmakingArgs {
start: chrono::DateTime<chrono::Local>,
text: String,
}
impl FromStr for MatchmakingArgs {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[(?<start>.*)]\s*(?<text>.+)$").unwrap());
let captures = REGEX.captures(s)
.context("Sintassi del comando incorretta.")?;
let start = captures.name("start")
.unwrap()
.as_str();
let text = captures.name("text")
.unwrap()
.as_str()
.to_string();
let start = parse_datetime_at_date(chrono::Local::now(), start)
.context("Impossibile determinare la data in cui l'attesa avrà termine.")?
.with_timezone(&chrono::Local);
Ok(
Self { start, text }
)
}
}
pub async fn handler(bot: &Bot, message: &Message, args: &MatchmakingArgs, database: &DatabaseInterface) -> CommandResult {
let mut database = database.connect()?;
let event = MatchmakingEvent::create(&mut database, &args.text, &args.start)
.context("Non è stato possibile creare un nuovo matchmaking.")?;
let mm1 = MatchmakingMessageTelegram::send_new_and_create(&mut database, event.id, bot, message.chat.id, Some(message.id))
.await
.context("Non è stato possibile postare il matchmaking.")?;
sleep_chrono(&args.start).await;
let _mm2 = MatchmakingMessageTelegram::send_new_and_create(&mut database, event.id, bot, message.chat.id, Some(message.id))
.await
.context("Non è stato possibile confermare il matchmaking.")?;
mm1.destroy_and_send_delete(&mut database, bot)
.await
.context("Non è stato possibile eliminare il matchmaking.")?;
Ok(())
}

View file

@ -2,25 +2,30 @@
// https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/dispatching_features.rs
use std::sync::Arc;
use anyhow::{Context, Error, Result};
use anyhow::{Context, Error};
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{ChatId, Message, MessageId};
use teloxide::types::{Message, ReplyParameters};
use teloxide::utils::command::BotCommands;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
use crate::utils::anyhow_result::AnyResult;
mod start;
mod fortune;
mod echo;
mod help;
mod whoami;
mod answer;
mod reminder;
mod dog;
mod cat;
mod roll;
mod diario;
pub mod start;
pub mod fortune;
pub mod echo;
pub mod help;
pub mod whoami;
pub mod answer;
pub mod reminder;
pub mod dog;
pub mod cat;
pub mod roll;
pub mod diario;
pub mod matchmaking;
type CommandResult = AnyResult<()>;
#[derive(Debug, Clone, PartialEq, Eq, BotCommands)]
#[command(rename_rule = "lowercase")]
@ -47,87 +52,123 @@ pub enum Command {
Roll(String),
#[command(description = "Salva una citazione nel diario RYG.")]
Diario(diario::DiarioArgs),
#[command(description = "Chiedi chi è disponibile per giocare a qualcosa.")]
Matchmaking(matchmaking::MatchmakingArgs),
}
impl Command {
pub async fn set_commands(bot: &mut Bot) -> Result<()> {
/// Update the [commands menu](https://core.telegram.org/bots/features#commands) of the bot.
pub async fn set_commands(bot: &mut Bot) -> AnyResult<()> {
log::debug!("Setting bot commands...");
log::trace!("Determining bot commands...");
let commands = Self::bot_commands();
// This always returns true, for whatever reason
log::trace!("Setting commands: {commands:#?}");
let _ = bot.set_my_commands(commands).await
.context("Impossibile aggiornare l'elenco comandi del bot.")?;
bot.set_my_commands(commands).await
.context("Non è stato possibile aggiornare la lista comandi del bot.")?;
log::trace!("Setting commands successful!");
Ok(())
}
pub async fn handle(self, bot: Bot, message: Message, database: Arc<DatabaseInterface>) -> CommandResult {
log::trace!("Handling command: {self:?}");
pub async fn handle_self(self, bot: Bot, message: Message, database: Arc<DatabaseInterface>) -> CommandResult {
log::debug!("Handling command...");
let result = match self {
Command::Start => start::handler(&bot, &message).await,
Command::Help(target) => match target.as_str() {
"" => help::handler_all(&bot, &message).await,
_ => help::handler_specific(&bot, &message, &target).await,
},
Command::Fortune => fortune::handler(&bot, &message).await,
Command::Echo(text) => echo::handler(&bot, &message, &text).await,
Command::WhoAmI => whoami::handler(&bot, &message, &database).await,
Command::Answer(_) => answer::handler(&bot, &message).await,
Command::Reminder(args) => reminder::handler(&bot, &message, args).await,
Command::Dog => dog::handler(&bot, &message).await,
Command::Cat => cat::handler(&bot, &message).await,
Command::Roll(roll) => roll::handler(&bot, &message, &roll).await,
Command::Diario(args) => diario::handler(&bot, &message, args, &database).await,
};
log::trace!(
"Handling {:?} in {:?} with {:?}...",
self,
&message.chat.id,
&message.id,
);
if result.is_ok() {
return Ok(())
}
// FIXME: Quick hack to fix single thread
log::trace!("Spawning task for future...");
let _task = tokio::spawn(async move {
log::trace!("Delegating command handling to handler...");
let result1 = match self {
Command::Start => start::handler(&bot, &message).await,
Command::Help(ref target) => match target.as_str() {
"" => help::handler_all(&bot, &message).await,
_ => help::handler_specific(&bot, &message, target).await,
},
Command::Fortune => fortune::handler(&bot, &message).await,
Command::Echo(ref text) => echo::handler(&bot, &message, text).await,
Command::WhoAmI => whoami::handler(&bot, &message, &database).await,
Command::Answer(_) => answer::handler(&bot, &message).await,
Command::Reminder(ref args) => reminder::handler(&bot, &message, args).await,
Command::Dog => dog::handler(&bot, &message).await,
Command::Cat => cat::handler(&bot, &message).await,
Command::Roll(ref roll) => roll::handler(&bot, &message, roll).await,
Command::Diario(ref args) => diario::handler(&bot, &message, args, &database).await,
Command::Matchmaking(ref args) => matchmaking::handler(&bot, &message, args, &database).await,
};
let chat_id = message.chat.id;
let message_id = message.id;
let error = result.unwrap_err();
log::trace!("Delegating error handling to error handler...");
let result2 = match result1.as_ref() {
Ok(_) => return,
Err(e1) => self.handle_error(&bot, &message, e1).await
};
let result2 = error_command(&bot, chat_id, message_id, &error).await;
let e1 = result1.unwrap_err();
if result2.is_ok() {
return Ok(())
}
log::trace!("Delegating fatal error handling to fatal error handler...");
let _result3 = match result2 {
Ok(_) => return,
Err(e2) => self.handle_fatal(&bot, &message, &e1, &e2).await
};
let error2 = result2.unwrap_err();
log::trace!("Successfully handled command!");
});
log::error!("Command message {message_id:?} in {chat_id:?} errored out with `{error}`, and it was impossible to handle the error because of `{error2}`\n\n{error2:?}");
log::trace!("Successfully spawned task!");
Ok(())
}
pub async fn handle_unknown(bot: Bot, message: Message) -> CommandResult {
log::debug!("Received an unknown command or an invalid syntax: {:?}", message.text());
log::trace!("Sending error message...");
let _reply = bot
.send_message(message.chat.id, "⚠️ Comando sconosciuto o sintassi non valida.")
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare il messaggio di errore.")?;
log::trace!("Successfully handled unknown command!");
Ok(())
}
async fn handle_error(&self, bot: &Bot, message: &Message, error: &Error) -> CommandResult {
log::debug!(
"Command message in {:?} with id {:?} and contents {:?} errored out with `{:?}`",
&message.chat.id,
&message.id,
self,
error,
);
log::trace!("Sending error message...");
let _reply = bot
.send_message(message.chat.id, format!("⚠️ {error}"))
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare il messaggio di errore.")?;
log::trace!("Successfully handled errored command!");
Ok(())
}
async fn handle_fatal(&self, _bot: &Bot, message: &Message, error1: &Error, error2: &Error) -> CommandResult {
log::error!(
"Command message in {:?} with id {:?} and contents {:?} errored out with `{:?}`, and it was impossible to handle the error because of `{:?}`",
&message.chat.id,
&message.id,
self,
error1,
error2,
);
Ok(())
}
}
async fn error_command(bot: &Bot, chat_id: ChatId, message_id: MessageId, error: &Error) -> CommandResult {
log::debug!("Command message {message_id:?} in {chat_id:?} errored out with `{error}`");
let text = format!("⚠️ {error}");
let _reply = bot
.send_message(chat_id, text)
.reply_to_message_id(message_id)
.await
.context("Non è stato possibile inviare la risposta.")?;
Ok(())
}
pub async fn unknown_command(bot: Bot, message: Message) -> CommandResult {
log::debug!("Received an unknown command.");
bot.send_message(message.chat.id, "⚠️ Comando sconosciuto o sintassi non valida.")
.reply_to_message_id(message.id)
.await
.context("Non è stato possibile inviare la risposta.")?;
Ok(())
}
type CommandResult = Result<()>;

View file

@ -3,23 +3,15 @@ use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Message, ParseMode};
use teloxide::types::{Message, ParseMode, ReplyParameters};
use parse_datetime::parse_datetime_at_date;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::utils::telegramdisplay::TelegramEscape;
use super::{CommandResult};
use crate::utils::telegram_string::TelegramEscape;
use crate::utils::time::sleep_chrono;
use super::CommandResult;
fn determine_wait(target_chrono: chrono::DateTime<chrono::Local>) -> tokio::time::Duration {
let now_chrono = chrono::Local::now();
let duration_chrono = target_chrono.signed_duration_since(now_chrono);
let seconds = duration_chrono.num_seconds();
tokio::time::Duration::from_secs(seconds as u64)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReminderArgs {
target: chrono::DateTime<chrono::Local>,
@ -54,7 +46,7 @@ impl FromStr for ReminderArgs {
}
}
pub async fn handler(bot: &Bot, message: &Message, ReminderArgs { target, reminder}: ReminderArgs) -> CommandResult {
pub async fn handler(bot: &Bot, message: &Message, ReminderArgs { target, reminder }: &ReminderArgs) -> CommandResult {
let text = format!(
"🕒 <b>Promemoria impostato</b>\n\
<i>{}</i>\n\
@ -67,13 +59,11 @@ pub async fn handler(bot: &Bot, message: &Message, ReminderArgs { target, remind
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la conferma.")?;
.context("Non è stato possibile inviare la conferma del promemoria.")?;
let wait_duration = determine_wait(target);
tokio::time::sleep(wait_duration).await;
sleep_chrono(target).await;
let text = format!(
"🕒 <b>Promemoria attivato</b>\n\
@ -87,7 +77,7 @@ pub async fn handler(bot: &Bot, message: &Message, ReminderArgs { target, remind
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare il promemoria.")?;

View file

@ -5,7 +5,7 @@ use teloxide::payloads::SendMessageSetters;
use teloxide::prelude::{Message, Requester};
use crate::services::telegram::commands::{CommandResult};
use regex::Regex;
use teloxide::types::ReplyParameters;
pub async fn handler(bot: &Bot, message: &Message, roll: &str) -> CommandResult {
let mut rng = rand::rngs::SmallRng::from_entropy();
@ -13,14 +13,14 @@ pub async fn handler(bot: &Bot, message: &Message, roll: &str) -> CommandResult
if rng.gen_range(1..1001) == 1 {
let _reply = bot
.send_message(message.chat.id, "🎶 Roll? Rick roll! https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;
return Ok(())
}
let re = Regex::new(r#"(?P<qty>[0-9]*)?d(?P<die>[0-9]+)(?P<modifier>[+-]?[0-9]*)?"#).unwrap();
let re = Regex::new(r#"(?P<qty>[0-9]*)?d(?P<die>[0-9]+)(?P<modifier>[+-]+[0-9]+)?"#).unwrap();
let captures = re.captures(roll)
.context("Sintassi dei dadi non corretta.")?;
@ -79,7 +79,7 @@ pub async fn handler(bot: &Bot, message: &Message, roll: &str) -> CommandResult
let _reply = bot
.send_message(message.chat.id, answer)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -2,12 +2,12 @@ use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Message};
use teloxide::types::{Message, ReplyParameters};
use super::{CommandResult};
pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let author = message.from()
let author = message.from.as_ref()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let author_username = match author.username.as_ref() {
@ -38,7 +38,7 @@ pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
let _reply = bot
.send_message(message.chat.id, text)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -2,14 +2,14 @@ use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Message, ParseMode};
use crate::interfaces::database::models::{RoyalnetUser};
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::telegramdisplay::TelegramEscape;
use super::{CommandResult};
use teloxide::types::{Message, ParseMode, ReplyParameters};
use crate::interfaces::database::models::RoyalnetUser;
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
use crate::utils::telegram_string::TelegramEscape;
use super::CommandResult;
pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface) -> CommandResult {
let author = message.from()
let author = message.from.as_ref()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let mut database = database.connect()?;
@ -19,7 +19,6 @@ pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface)
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>(
@ -42,7 +41,7 @@ pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface)
let _reply = bot
.send_message(message.chat.id, text)
.parse_mode(ParseMode::Html)
.reply_to_message_id(message.id)
.reply_parameters(ReplyParameters::new(message.id))
.await
.context("Non è stato possibile inviare la risposta.")?;

View file

@ -1,6 +1,6 @@
use anyhow::Context;
use diesel::PgConnection;
use crate::utils::result::AnyResult;
use crate::utils::anyhow_result::AnyResult;
#[derive(Debug, Clone)]
pub struct DatabaseInterface {
@ -14,6 +14,6 @@ impl DatabaseInterface {
pub fn connect(&self) -> AnyResult<PgConnection> {
crate::interfaces::database::connect(&self.database_url)
.context("Impossibile connettersi al database RYG")
.context("Impossibile connettersi al database RYG.")
}
}

View file

@ -0,0 +1,45 @@
use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::AnswerCallbackQuerySetters;
use teloxide::requests::Requester;
use teloxide::types::CallbackQuery;
use crate::interfaces::database::models::{MatchmakingChoice, MatchmakingId, MatchmakingMessageTelegram, MatchmakingReply, MatchmakingTelegramKeyboardCallback, RoyalnetUser};
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
use crate::services::telegram::keyboard_callbacks::KeyboardCallbackResult;
pub async fn handler(bot: &Bot, query: CallbackQuery, matchmaking_id: MatchmakingId, callback: MatchmakingTelegramKeyboardCallback, database: &DatabaseInterface) -> KeyboardCallbackResult {
use MatchmakingTelegramKeyboardCallback::*;
let mut database = database.connect()
.context("Non è stato possibile connettersi al database RYG.")?;
let royalnet_user = RoyalnetUser::from_telegram_userid(&mut database, query.from.id)
.context("Non è stato possibile recuperare il tuo utente Telegram dal database RYG.")?;
match callback {
Yes => MatchmakingReply::set(&mut database, matchmaking_id, royalnet_user.id, MatchmakingChoice::Yes)?,
Plus5Min => MatchmakingReply::add_late_minutes(&mut database, matchmaking_id, royalnet_user.id, 5)?,
Plus15Min => MatchmakingReply::add_late_minutes(&mut database, matchmaking_id, royalnet_user.id, 15)?,
Plus60Min => MatchmakingReply::add_late_minutes(&mut database, matchmaking_id, royalnet_user.id, 60)?,
Maybe => MatchmakingReply::set(&mut database, matchmaking_id, royalnet_user.id, MatchmakingChoice::Maybe)?,
DontWait => MatchmakingReply::set(&mut database, matchmaking_id, royalnet_user.id, MatchmakingChoice::DontWait)?,
Cant => MatchmakingReply::set(&mut database, matchmaking_id, royalnet_user.id, MatchmakingChoice::Cant)?,
Wont => MatchmakingReply::set(&mut database, matchmaking_id, royalnet_user.id, MatchmakingChoice::Wont)?,
};
let messages_telegram = MatchmakingMessageTelegram::get_all(&mut database, matchmaking_id)
.context("Non è stato possibile recuperare i messaggi di matchmaking inviati su Telegram.")?;
for message_telegram in messages_telegram {
message_telegram.make_text_and_send_edit(&mut database, bot)
.await
.context("Non è stato possibile aggiornare un messaggio di matchmaking su Telegram.")?;
}
let _ = bot.answer_callback_query(query.id)
.text("Ricevuto!")
.await
.context("Non è stato possibile rispondere alla pressione del bottone su Telegram.")?;
Ok(())
}

View file

@ -0,0 +1,80 @@
mod matchmaking;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::Context;
use teloxide::Bot;
use teloxide::payloads::AnswerCallbackQuerySetters;
use teloxide::prelude::CallbackQuery;
use teloxide::requests::Requester;
use crate::interfaces::database::models::{MatchmakingId, MatchmakingTelegramKeyboardCallback};
use crate::services::telegram::dependencies::interface_database::DatabaseInterface;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyboardCallback {
Matchmaking(MatchmakingId, MatchmakingTelegramKeyboardCallback),
}
impl FromStr for KeyboardCallback {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (keyword, data) = s.split_once(":")
.context("Impossibile dividere il payload in keyword e dati.")?;
let (id, data) = data.split_once(":")
.context("Impossibile dividere il payload in id e dati.")?;
let id: MatchmakingId = id.parse()
.context("Impossibile convertire l'id a un numero.")?;
match keyword {
"matchmaking" => {
data
.parse()
.map(|c| Self::Matchmaking(id, c))
.context("Impossibile processare i dati.")
},
x => {
anyhow::bail!("Keyword sconosciuta: {x:?}")
}
}
}
}
impl KeyboardCallback {
pub async fn handle_self(self, bot: Bot, query: CallbackQuery, database: Arc<DatabaseInterface>) -> KeyboardCallbackResult {
log::debug!("Handling keyboard callback...");
log::trace!(
"Handling {:?} in {:?} with {:?}...",
self,
&query.message.as_ref().map(|q| q.chat().id),
&query.id,
);
match self {
Self::Matchmaking(matchmaking_id, callback) => {
matchmaking::handler(&bot, query, matchmaking_id, callback, &database).await?
}
}
log::trace!("Successfully handled keyboard callback!");
Ok(())
}
pub async fn handle_unknown(bot: Bot, query: CallbackQuery) -> KeyboardCallbackResult {
log::warn!("Received an unknown keyboard callback: {:#?}", &query.data);
bot
.answer_callback_query(query.id)
.show_alert(true)
.text("⚠️ Il tasto che hai premuto non è più valido.")
.await?;
log::trace!("Successfully handled unknown keyboard callback!");
Ok(())
}
}
type KeyboardCallbackResult = anyhow::Result<()>;

View file

@ -1,3 +1,8 @@
mod commands;
mod dependencies;
mod keyboard_callbacks;
mod utils;
use std::sync::Arc;
use anyhow::Context;
use teloxide::prelude::*;
@ -5,15 +10,13 @@ use teloxide::types::{Me, ParseMode};
use regex::Regex;
use teloxide::dispatching::DefaultKey;
use teloxide::dptree::entry;
use crate::services::telegram::commands::Command;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::result::{AnyError, AnyResult};
use crate::utils::telegramdisplay::TelegramEscape;
use commands::Command;
use dependencies::interface_database::DatabaseInterface;
use keyboard_callbacks::KeyboardCallback;
use crate::utils::anyhow_result::{AnyError, AnyResult};
use crate::utils::telegram_string::TelegramEscape;
use super::RoyalnetService;
mod commands;
mod deps;
#[derive(Debug, Clone)]
pub struct TelegramService {
database_url: String,
@ -110,33 +113,51 @@ impl TelegramService {
log::trace!("Building dispatcher...");
Dispatcher::builder(
self.bot.clone(),
// Only process message updates
Update::filter_message()
// Pseudo-commands
.branch(entry()
// Only process commands matching the pseudo-command regex
.filter(move |message: Message| -> bool {
message
.text()
.is_some_and(|text| regex.is_match(text))
})
// Commands
.branch(
entry()
// When an update is received
entry()
// Messages
.branch(Update::filter_message()
// Pseudo-commands
.branch(entry()
// Only process commands matching the pseudo-command regex
.filter(move |message: Message| -> bool {
message
.text()
.is_some_and(|text| regex.is_match(text))
})
// Commands
.branch(entry()
// Only process commands matching a valid command, and parse their arguments
.filter_command::<Command>()
// Delegate handling
.endpoint(Command::handle)
.endpoint(Command::handle_self)
)
// No valid command was found
.endpoint(Command::handle_unknown)
)
// No valid command was found
.endpoint(commands::unknown_command)
)
)
// Inline keyboard
.branch(Update::filter_callback_query()
// Known callbacks
.branch(entry()
// Only process queries that match
.filter_map(move |query: CallbackQuery| query.data
// Parse the data string as a KeyboardCallback
.and_then(|data| data.parse::<KeyboardCallback>().ok())
)
.endpoint(KeyboardCallback::handle_self)
)
.endpoint(KeyboardCallback::handle_unknown)
)
)
.dependencies(
dptree::deps![
database
]
)
.default_handler(|upd| async move {
log::trace!("Unhandled update: {:?}", upd);
})
.build()
}

View file

@ -0,0 +1,24 @@
use anyhow::Context;
use diesel::PgConnection;
use teloxide::types::UserId;
use crate::interfaces::database::models::RoyalnetUser;
use crate::utils::anyhow_result::AnyResult;
impl RoyalnetUser {
pub fn from_telegram_userid(database: &mut PgConnection, user_id: UserId) -> AnyResult<Self> {
use crate::interfaces::database::query_prelude::*;
use schema::{telegram, users};
log::trace!("Retrieving RoyalnetUser with {user_id:?}");
telegram::table
.filter(telegram::telegram_id.eq::<i64>(
user_id.0.try_into()
.context("Lo user_id specificato non può essere interpretato come un numero signed, il che lo rende incompatibile con il database RYG.")?
))
.inner_join(users::table)
.select(RoyalnetUser::as_select())
.get_result(database)
.context("Non è stato possibile recuperare l'utente Telegram specificato dal database RYG.")
}
}

View file

@ -0,0 +1 @@
mod database;

View file

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

View file

@ -1,7 +1,7 @@
use std::fmt::Write;
use std::fmt::{Error, Write};
pub trait TelegramWrite {
fn write_telegram<T>(&self, f: &mut T) -> Result<(), std::fmt::Error>
fn write_telegram<T>(&self, f: &mut T) -> Result<(), Error>
where T: Write;
fn to_string_telegram(&self) -> String {
@ -16,7 +16,7 @@ pub trait TelegramEscape {
}
impl<T> TelegramEscape for T
where String: From<T>
where String: From<T>
{
fn escape_telegram_html(self) -> String {
String::from(self)
@ -24,4 +24,4 @@ impl<T> TelegramEscape for T
.replace(">", "&gt;")
.replace("&", "&amp;")
}
}
}

View file

@ -0,0 +1,18 @@
pub fn chrono_to_tokio_duration(duration: chrono::TimeDelta) -> Option<tokio::time::Duration> {
let nanos = duration.num_nanoseconds()?;
Some(
tokio::time::Duration::from_nanos(nanos as u64)
)
}
pub async fn sleep_chrono(until: &chrono::DateTime<chrono::Local>) {
let now = chrono::Local::now();
let duration = until.signed_duration_since(now);
let duration = chrono_to_tokio_duration(duration)
.expect("Nanoseconds to not overflow u64");
tokio::time::sleep(duration).await;
}