1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-25 12:34:19 +00:00

Massive amount of changes

This commit is contained in:
Steffo 2024-07-17 14:01:53 +02:00
parent 8afbb1c421
commit f8c77ef264
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
35 changed files with 1276 additions and 769 deletions

View file

@ -5,7 +5,7 @@
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />

View file

@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run (debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package royalnet --bin royalnet" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs>
<env name="RUST_LOG" value="royalnet" />
</envs>
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

28
Cargo.lock generated
View file

@ -137,9 +137,9 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "cc"
version = "1.1.3"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e2d530f35b40a84124146478cd16f34225306a8441998836466a2e2961c950"
checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052"
[[package]]
name = "cfg-if"
@ -1423,7 +1423,7 @@ dependencies = [
[[package]]
name = "royalnet"
version = "0.3.2"
version = "0.4.0"
dependencies = [
"anyhow",
"chrono",
@ -1543,9 +1543,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
@ -1556,9 +1556,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
dependencies = [
"core-foundation-sys",
"libc",
@ -1625,15 +1625,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -1877,9 +1868,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.38.0"
version = "1.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df"
dependencies = [
"backtrace",
"bytes",
@ -1887,7 +1878,6 @@ dependencies = [
"mio",
"num_cpus",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",

View file

@ -1,7 +1,7 @@
[package]
name = "royalnet"
description = "Fun software suite for the RYG community"
version = "0.3.2"
version = "0.4.0"
edition = "2021"
authors = [
"Stefano Pigozzi <me@steffo.eu>"
@ -26,23 +26,101 @@ exclude = [
"/.env"
]
#============#
[dependencies]
anyhow = "1.0.86"
chrono = "0.4.38"
diesel = { version = "2.2.1", features = ["postgres"] }
log = { version = "0.4.22", features = ["release_max_level_debug"] }
micronfig = "0.3.0"
pretty_env_logger = "0.5.0"
rand = { version = "0.8.5", features = ["small_rng"] }
teloxide = { version = "0.12.2", features = ["ctrlc_handler", "native-tls", "macros"], default-features = false }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread", "time"] }
parse_datetime = "0.6.0"
regex = "1.10.5"
once_cell = "1.19.0"
reqwest = { version = "0.12.5", features = ["json"] }
serde = { version = "1.0.204", features = ["derive"] }
graphql_client = "0.14.0"
thiserror = "1.0.62"
[dependencies.anyhow]
version = "1.0.86"
[dependencies.thiserror]
version = "1.0.62"
[dependencies.tokio]
version = "1.38.0"
features = ["macros", "rt-multi-thread", "time"]
[dependencies.log]
version = "0.4.22"
features = ["release_max_level_debug"]
[dependencies.pretty_env_logger]
version = "0.5.0"
[dependencies.micronfig]
version = "0.3.0"
[dependencies.once_cell]
version = "1.19.0"
[dependencies.regex]
version = "1.10.5"
[dependencies.reqwest]
version = "0.12.5"
features = ["json"]
[dependencies.serde]
version = "1.0.204"
features = ["derive"]
[dependencies.diesel]
version = "2.2.1"
features = ["postgres"]
optional = true
[dependencies.teloxide]
version = "0.12.2"
default-features = false
features = ["native-tls", "macros"]
optional = true
[dependencies.rand]
version = "0.8.5"
features = ["small_rng"]
optional = true
[dependencies.chrono]
version = "0.4.38"
optional = true
[dependencies.parse_datetime]
version = "0.6.0"
optional = true
[dependencies.graphql_client]
version = "0.14.0"
optional = true
#============#
[features]
default = [
"interface_database",
"interface_stratz",
"service_brooch",
"service_telegram",
]
interface_database = [
"diesel"
]
interface_stratz = [
"graphql_client"
]
service_telegram = [
"interface_database",
"teloxide",
"rand",
"chrono",
"parse_datetime"
]
service_brooch = [
"interface_database",
"interface_stratz",
"graphql_client"
]
#============#
[[bin]]
name = "royalnet"

View file

@ -1,5 +0,0 @@
use micronfig::config;
config! {
DATABASE_URL,
}

View file

@ -1,9 +0,0 @@
use diesel::{Connection, ConnectionResult, PgConnection};
mod config;
pub mod schema;
pub mod models;
pub fn connect() -> ConnectionResult<PgConnection> {
PgConnection::establish(config::DATABASE_URL())
}

60
src/instance/config.rs Normal file
View file

@ -0,0 +1,60 @@
#![allow(unused_attributes, unused_qualifications, clippy::needless_pub_self)]
#[cfg(feature = "service_telegram")]
pub mod service_telegram {
use micronfig::config;
config! {
TELEGRAM_DATABASE_URL: String,
TELEGRAM_BOT_TOKEN: String,
TELEGRAM_NOTIFICATION_CHATID?: String > i64 -> crate::instance::config::ChatIdConversionHack -> teloxide::types::ChatId,
}
}
#[cfg(feature = "service_brooch")]
pub mod brooch {
use micronfig::config;
#[allow(unused_qualifications)]
config! {
BROOCH_DATABASE_URL: String,
BROOCH_GRAPHQL_URL: String,
BROOCH_STRATZ_TOKEN: String,
BROOCH_TELEGRAM_BOT_TOKEN: String,
BROOCH_WATCHED_GUILD_ID: String > i64,
BROOCH_MIN_PLAYERS_TO_PROCESS: String > usize,
BROOCH_NOTIFICATION_CHAT_ID: String > i64 -> crate::instance::config::ChatIdConversionHack -> teloxide::types::ChatId,
BROOCH_MAX_IMP_WAIT_SECS: String > i64 -> crate::instance::config::TimeDeltaConversionHack => chrono::TimeDelta,
}
}
pub struct ChatIdConversionHack(i64);
impl From<i64> for ChatIdConversionHack {
fn from(value: i64) -> Self {
Self(value)
}
}
impl From<ChatIdConversionHack> for teloxide::types::ChatId {
fn from(value: ChatIdConversionHack) -> Self {
Self(value.0)
}
}
pub struct TimeDeltaConversionHack(i64);
impl From<i64> for TimeDeltaConversionHack {
fn from(value: i64) -> Self {
Self(value)
}
}
impl TryFrom<TimeDeltaConversionHack> for chrono::TimeDelta {
type Error = ();
fn try_from(value: TimeDeltaConversionHack) -> Result<Self, Self::Error> {
Self::new(value.0, 0).ok_or(())
}
}

102
src/instance/mod.rs Normal file
View file

@ -0,0 +1,102 @@
use std::future::Future;
use crate::services::RoyalnetService;
pub(self) mod config;
pub struct RoyalnetInstance {
#[cfg(feature = "service_telegram")]
service_telegram: crate::services::telegram::TelegramService,
#[cfg(feature = "service_brooch")]
service_brooch: crate::services::brooch::BroochService,
}
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,
}
}
pub async fn run(mut self) {
let future_telegram = async move {
Self::get_telegram_future(&mut self.service_telegram).await;
};
let future_brooch = async move {
Self::get_brooch_future(&mut self.service_brooch).await;
};
let task_telegram = tokio::spawn(future_telegram);
let task_brooch = tokio::spawn(future_brooch);
let _ = tokio::join!(
task_telegram,
task_brooch,
);
}
#[cfg(feature = "service_telegram")]
async fn setup_telegram_service() -> crate::services::telegram::TelegramService {
log::debug!("Setting up Telegram service...");
crate::services::telegram::TelegramService::new(
config::service_telegram::TELEGRAM_DATABASE_URL().clone(),
config::service_telegram::TELEGRAM_BOT_TOKEN().clone(),
config::service_telegram::TELEGRAM_NOTIFICATION_CHATID().clone(),
).await.expect("Unable to setup Telegram service.")
}
#[cfg(not(feature = "service_telegram"))]
async fn setup_telegram_service() -> () {
log::warn!("Telegram service is disabled.");
()
}
#[cfg(feature = "service_telegram")]
fn get_telegram_future(service: &mut crate::services::telegram::TelegramService) -> impl Future<Output = ()> + '_ {
service.run_loop()
}
#[cfg(not(feature = "service_telegram"))]
fn get_telegram_future(service: &mut crate::services::telegram::TelegramService) -> impl Future<Output = ()> + '_ {
async {}
}
#[cfg(feature = "service_brooch")]
fn setup_brooch_service() -> crate::services::brooch::BroochService {
log::debug!("Setting up Brooch service...");
crate::services::brooch::BroochService::new(
config::brooch::BROOCH_DATABASE_URL().clone(),
config::brooch::BROOCH_GRAPHQL_URL(),
config::brooch::BROOCH_STRATZ_TOKEN(),
config::brooch::BROOCH_WATCHED_GUILD_ID().clone(),
config::brooch::BROOCH_MIN_PLAYERS_TO_PROCESS().clone(),
config::brooch::BROOCH_TELEGRAM_BOT_TOKEN().clone(),
config::brooch::BROOCH_NOTIFICATION_CHAT_ID().clone(),
config::brooch::BROOCH_MAX_IMP_WAIT_SECS().clone(),
).expect("Unable to setup Brooch service.")
}
#[cfg(not(feature = "service_brooch"))]
fn setup_brooch_service() -> () {
log::warn!("Brooch service is disabled.");
()
}
#[cfg(feature = "service_brooch")]
fn get_brooch_future(service: &mut crate::services::brooch::BroochService) -> impl Future<Output = ()> + '_ {
service.run_loop()
}
#[cfg(not(feature = "service_brooch"))]
fn get_brooch_future(service: &mut crate::services::brooch::BroochService) -> impl Future<Output = ()> + '_ {
async {}
}
}

View file

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

View file

@ -44,6 +44,7 @@ pub struct SteamUser {
}
#[cfg(feature = "service_brooch")]
#[derive(Debug, Clone, PartialEq, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = brooch_match)]
#[diesel(check_for_backend(Pg))]

View file

@ -1,5 +1,6 @@
// @generated automatically by Diesel CLI.
#[cfg(feature = "service_brooch")]
diesel::table! {
brooch_match (id) {
id -> Int8,

5
src/interfaces/mod.rs Normal file
View file

@ -0,0 +1,5 @@
#[cfg(feature = "interface_database")]
pub mod database;
#[cfg(feature = "interface_stratz")]
pub mod stratz;

View file

@ -0,0 +1,2 @@
schema: "schema.json"
documents: "**/*.gql"

View file

@ -0,0 +1,54 @@
#![allow(unused_imports)]
use graphql_client::GraphQLQuery;
use reqwest::Url;
pub use super::Short;
pub use super::Long;
pub use super::Byte;
pub use super::QueryError as Error;
#[derive(graphql_client::GraphQLQuery)]
#[graphql(
schema_path = "src/interfaces/stratz/schema.json",
query_path = "src/interfaces/stratz/query_guild_matches.gql",
response_derives = "Debug, Clone"
)]
struct Query;
pub type QueryResponse = graphql_client::Response<query::ResponseData>;
pub type QueryResult = Result<QueryResponse, Error>;
pub use query::LobbyTypeEnum as LobbyType;
pub use query::GameModeEnumType as GameMode;
pub use query::MatchLaneType as Lane;
pub use query::MatchPlayerRoleType as Role;
pub use query::QueryGuild as Guild;
pub use query::QueryGuildMatches as Match;
pub use query::QueryGuildMatchesPlayers as Player;
pub use query::QueryGuildMatchesPlayersHero as Hero;
pub use query::QueryGuildMatchesPlayersSteamAccount as Steam;
pub use query::QueryGuildMatchesPlayersStatsMatchPlayerBuffEvent as Buff;
pub async fn query(client: &reqwest::Client, url: Url, guild_id: i64) -> QueryResult {
log::debug!("Querying guild_matches of guild {guild_id}...");
log::trace!("Using client: {client:?}");
log::trace!("Using API at: {url:?}");
log::trace!("Configuring query variables...");
let vars = query::Variables { guild_id };
log::trace!("Building query...");
let body = Query::build_query(vars);
log::trace!("Making request...");
let response = client.post(url)
.json(&body)
.send()
.await
.map_err(|_| Error::Requesting)?
.json::<QueryResponse>()
.await
.map_err(|_| Error::Parsing)?;
Ok(response)
}

View file

@ -0,0 +1,15 @@
use thiserror::Error;
pub type Short = i16;
pub type Long = i64;
pub type Byte = u8;
#[derive(Debug, Clone, Error)]
pub enum QueryError {
#[error("GraphQL request failed")]
Requesting,
#[error("GraphQL response parsing failed")]
Parsing,
}
pub mod guild_matches;

View file

@ -1,4 +1,4 @@
query GuildMatchesQuery($guild_id: Int!) {
query Query($guild_id: Int!) {
guild(id: $guild_id) {
id
matches(take: 10) {
@ -7,9 +7,9 @@ query GuildMatchesQuery($guild_id: Int!) {
gameMode
durationSeconds
endDateTime
didRadiantWin
players(steamAccountId: null) {
isRadiant
isVictory
imp
kills
deaths

View file

@ -1,41 +1,20 @@
use anyhow::Result;
use crate::services::RoyalnetService;
use crate::instance::RoyalnetInstance;
pub(crate) mod database;
pub(crate) mod utils;
mod instance;
mod interfaces;
mod services;
mod stratz;
pub(crate) mod utils;
#[tokio::main]
async fn main() -> Result<()> {
async fn main() {
// Logging setup
pretty_env_logger::init();
log::debug!("Logging initialized successfully!");
// Telegram setup
log::trace!("Setting up Telegram bot service...");
let telegram = services::telegram::BotService::from_config();
// Create instance
let instance = RoyalnetInstance::new().await;
// Brooch setup
log::trace!("Setting up Brooch service...");
let brooch = services::brooch::BroochService::from_config();
instance.run().await;
// Run all services concurrently
log::info!("Starting services...");
let result = tokio::try_join![
telegram.run(),
brooch.run(),
];
// This should never happen, but just in case...
match result {
Err(error) => {
log::error!("A service has exited with an error, bailing out: {error:?}");
anyhow::bail!("A service has exited with an error.")
},
_ => {
log::error!("All service have exited successfully, bailing out...");
anyhow::bail!("All service have exited successfully.")
}
}
log::error!("No services configured.");
}

View file

@ -1,7 +0,0 @@
use micronfig::config;
config! {
BROOCH_TELEGRAM_BOT_TOKEN,
BROOCH_WATCHED_GUILD_ID: String > i64 -> crate::stratz::GuildId,
BROOCH_NOTIFICATION_CHAT_ID: String > i64 -> crate::utils::hacks::ChatIdConversionHack -> teloxide::types::ChatId
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,40 @@
use std::convert::Infallible;
use anyhow::Result;
pub mod telegram;
pub mod brooch;
use std::time::Duration;
use tokio::time::sleep;
use crate::utils::result::AnyResult;
pub trait RoyalnetService {
async fn run(self) -> Result<Infallible>;
async fn run(&mut self) -> AnyResult<()>;
async fn run_loop(&mut self) {
let mut backoff = Duration::new(1, 0);
loop {
let result = self.run().await;
match result {
Err(e) => {
log::error!("Service exited with error: {e:?}.")
},
_ => {
log::debug!("Service exited successfully!")
},
}
let backoff_secs = backoff.as_secs();
log::debug!("Backing off for {backoff_secs} seconds before restarting...");
sleep(backoff).await;
log::trace!("Doubling backoff value...");
backoff *= 2;
log::trace!("Backoff value is now {backoff_secs} seconds.");
}
}
}
#[cfg(feature = "service_telegram")]
pub mod telegram;
#[cfg(feature = "service_brooch")]
pub mod brooch;

View file

@ -7,6 +7,7 @@ use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{ChatId, Message, MessageId};
use teloxide::utils::command::BotCommands;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
mod start;
mod fortune;
@ -58,7 +59,7 @@ impl Command {
Ok(())
}
pub async fn handle(self, bot: Bot, message: Message) -> CommandResult {
pub async fn handle(self, bot: Bot, message: Message, database: &DatabaseInterface) -> CommandResult {
log::trace!("Handling command: {self:?}");
let result = match self {
@ -69,7 +70,7 @@ impl Command {
},
Command::Fortune => fortune::handler(&bot, &message).await,
Command::Echo(text) => echo::handler(&bot, &message, &text).await,
Command::WhoAmI => whoami::handler(&bot, &message).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,

View file

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

View file

@ -7,9 +7,9 @@ use crate::services::telegram::commands::{CommandResult};
use regex::Regex;
pub async fn handler(bot: &Bot, message: &Message, roll: &str) -> CommandResult {
let mut rng = rand::rngs::SmallRng::from_entropy();
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")
@ -21,25 +21,29 @@ pub async fn handler(bot: &Bot, message: &Message, roll: &str) -> CommandResult
}
let re = Regex::new(r#"(?P<qty>[0-9]*)?d(?P<die>[0-9]+)(?P<modifier>[+-]?[0-9]*)?"#).unwrap();
let qty = captures.name("qty") // Prova a vedere se c'è il gruppo "qty"
.map(|m| m.as_str()) // `map`: se c'è, trasforma il suo contenuto in stringa
.map(|m| m.parse::<u32>()) // `map`: se c'è, trasforma la stringa in un u32
.map(|m| m.context("La quantità di dadi da lanciare deve essere un numero intero positivo diverso da 0.")?) // `map`: se c'è, ma il parsing ha dato errore, restituiscilo e fai terminare la funzione qui
.unwrap_or(1); // `unwrap_or`: se c'è, restituisci il valore, altrimenti, defaulta a 1
let die = captures.name("die") // Prova a vedere se c'è il gruppo "die"
.unwrap() // `unwrap`: possiamo asserire che il gruppo "die" sia sempre presente se la regex ha matchato
.as_str() // trasforma il suo contenuto in stringa
.parse::<u32>() // trasforma la stringa in un u32
.context("La dimensione del dado da lanciare deve essere un numero intero positivo.")?; // se il parsing ha dato errore, restituiscilo e fai terminare la funzione qui
let captures = re.captures(roll)
.context("Sintassi dei dadi non corretta.")?;
let modifier = captures.name("modifier") // Prova a vedere se c'è il gruppo "modifier"
.map(|m| m.as_str()) // `map`: se c'è, trasforma il suo contenuto in stringa
.map(|m| m.parse::<i32>()) // `map`: se c'è, trasforma la stringa in un i32
.map(|m| m.context("Il modificatore dei dadi lanciati deve essere un numero intero.")?) // `map`: se c'è, ma il parsing ha dato errore, restituiscilo e fai terminare la funzione qui
.unwrap_or(0); // `unwrap_or`: se c'è, restituisci il valore, altrimenti, defaulta a 0
let qty = captures.name("qty")
.map(|m| m.as_str())
.map(|m| m.parse::<u32>())
.unwrap_or(Ok(1))
.context("La quantità di dadi da lanciare deve essere un numero intero positivo diverso da 0.")?;
if die <= 0 {
let die = captures.name("die")
.unwrap()
.as_str()
.parse::<u32>()
.context("La dimensione del dado da lanciare deve essere un numero intero positivo.")?;
let modifier = captures.name("modifier")
.map(|m| m.as_str())
.map(|m| m.parse::<i32>())
.unwrap_or(Ok(0))
.context("Il modificatore dei dadi lanciati deve essere un numero intero.")?;
if die == 0 {
anyhow::bail!("Non è stato specificato nessun dado.")
}
@ -47,37 +51,32 @@ let qty = captures.name("qty") // Prova a vedere se c'è il gruppo "qty"
anyhow::bail!("La quantità di dadi specificata deve essere un intero positivo.")
}
let mut nums_rolled = Vec::<i32>::new();
let mut nums_rolled = Vec::<u32>::new();
for _ in 0..qty {
nums_rolled.push(rng.gen_range(1..die+1));
nums_rolled.push(
rng.gen_range(1..=die)
);
}
let roll_string = nums_rolled
.iter()
.map(|n| n.to_string())
.collect::<Vec<String>>()
.join("\n");
let mut answer = String::from("🎲 [");
for i in 0..qty {
if i > 0 { answer.push_str("+")}
answer.push_str( &nums_rolled[i].to_string() );
}
answer.push_str("] ");
let mut answer = format!("🎲 [{roll_string}]");
if modifier != 0 {
if modifier > 0 {
answer.push_str("+");
}
answer.push_str( &modifier.to_string() );
answer.push_str(&format!("{modifier:+}"))
}
answer.push_str(" = ");
let mut sum: i32 = nums_rolled.iter().sum();
sum = sum + modifier;
let sum: u32 = nums_rolled.iter().sum();
let sum: i32 = sum as i32 + modifier;
answer.push_str(&sum.to_string());
let _reply = bot
.send_message(message.chat.id, answer)
.reply_to_message_id(message.id)

View file

@ -3,23 +3,23 @@ use teloxide::Bot;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Message, ParseMode};
use crate::database::models::{RoyalnetUser};
use crate::services::telegram::escape::EscapableInTelegramHTML;
use crate::interfaces::database::models::{RoyalnetUser};
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::escape::EscapableInTelegramHTML;
use super::{CommandResult};
pub async fn handler(bot: &Bot, message: &Message) -> CommandResult {
pub async fn handler(bot: &Bot, message: &Message, database: &DatabaseInterface) -> CommandResult {
let author = message.from()
.context("Non è stato possibile determinare chi ha inviato questo comando.")?;
let mut database = crate::database::connect().
context("Non è stato possibile connettersi al database RYG.")?;
let mut database = database.connect()?;
let royalnet_user: RoyalnetUser = {
use diesel::prelude::*;
use diesel::{ExpressionMethods, QueryDsl};
use crate::database::schema::telegram::dsl::*;
use crate::database::schema::users::dsl::*;
use crate::database::models::RoyalnetUser;
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>(

View file

@ -1,7 +0,0 @@
use micronfig::config;
// Everything ok, RustRover?
config! {
TELEGRAM_BOT_TOKEN,
TELEGRAM_NOTIFICATION_CHATID?: String > i64 -> crate::utils::hacks::ChatIdConversionHack -> teloxide::types::ChatId,
}

View file

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

View file

@ -0,0 +1,2 @@
#[cfg(feature = "interface_database")]
pub mod interface_database;

View file

@ -1,113 +1,164 @@
use std::convert::Infallible;
use teloxide::{Bot, dptree};
use anyhow::{Context, Error, Result};
use anyhow::Context;
use teloxide::prelude::*;
use teloxide::types::{Me, ParseMode};
use regex::Regex;
use teloxide::dispatching::{DefaultKey, Dispatcher, HandlerExt, UpdateFilterExt};
use teloxide::dispatching::DefaultKey;
use teloxide::dptree::entry;
use teloxide::payloads::SendMessageSetters;
use teloxide::requests::Requester;
use teloxide::types::{Me, Message, ParseMode, Update};
use crate::services::telegram::escape::EscapableInTelegramHTML;
use crate::services::telegram::commands::Command;
use crate::services::telegram::deps::interface_database::DatabaseInterface;
use crate::utils::result::{AnyError, AnyResult};
use crate::utils::escape::EscapableInTelegramHTML;
use super::RoyalnetService;
#[allow(clippy::needless_pub_self)]
pub(self) mod config;
mod commands;
pub(self) mod escape;
pub struct BotService {
pub bot: Bot
pub(self) mod deps;
#[derive(Debug, Clone)]
pub struct TelegramService {
database_url: String,
bot: Bot,
me: Me,
notification_chat_id: Option<ChatId>,
}
impl BotService {
pub fn from_config() -> Self {
Self {
bot: Bot::new(config::TELEGRAM_BOT_TOKEN())
}
impl TelegramService {
pub async fn new(database_url: String, token: String, notification_chat_id: Option<ChatId>) -> AnyResult<Self> {
log::info!("Initializing a new Telegram service...");
let bot = Bot::new(token);
log::trace!("Using bot: {bot:#?}");
let me = Self::get_me(&bot)
.await?;
log::trace!("Using self details: {me:#?}");
let service = Self {
database_url,
bot,
me,
notification_chat_id
};
log::trace!("Created service: {service:#?}");
Ok(service)
}
async fn send_start_notification(&mut self, me: &Me) -> Result<()> {
let chat_id = config::TELEGRAM_NOTIFICATION_CHATID()
.context("Variabile d'ambiente TELEGRAM_NOTIFICATION_CHATID mancante.")?;
async fn get_me(bot: &Bot) -> AnyResult<Me> {
log::debug!("Getting self details...");
bot.get_me().await
.context("Recupero dettagli sul bot non riuscito.")
}
let version = crate::utils::version::VERSION;
let username = &me.username.as_ref().unwrap();
let id = &me.user.id;
async fn send_start_notification(&self) -> AnyResult<Message> {
log::debug!("Sending start notification...");
let notification_chat_id = self.notification_chat_id
.context("La chat di notifica non è abilitata.")?;
let version = crate::utils::version::VERSION
.escape_telegram_html();
let username = self.me.username
.as_ref()
.unwrap()
.escape_telegram_html();
let id = self.me.user.id
.to_string()
.escape_telegram_html();
let text = format!(
"💠 <b>Servizio Telegram avviato</b>\n\
\n\
Royalnet <a href='https://github.com/RYGhub/royalnet/releases/tag/v{}'>v{}</a>\n\
Royalnet <a href='https://github.com/RYGhub/royalnet/releases/tag/v{version}'>v{version}</a>\n\
\n\
@{} [<code>{}</code>]",
version.escape_telegram_html(),
version.escape_telegram_html(),
username.escape_telegram_html(),
id.to_string().escape_telegram_html(),
@{username} [<code>{id}</code>]"
);
self.bot.send_message(chat_id, text)
log::trace!("Sending start notification message...");
let msg = self.bot.send_message(notification_chat_id, text)
.parse_mode(ParseMode::Html)
.await
.context("Invio della notifica di avvio non riuscito.")?;
Ok(())
}
log::trace!("Successfully sent start notification message!");
Ok(msg)
}
impl RoyalnetService for BotService {
async fn run(mut self) -> Result<Infallible> {
log::info!("Starting Telegram service...");
log::debug!("Getting bot information...");
let me = self.bot.get_me().await
.context("Failed to get information about self")?;
log::debug!("Setting bot commands...");
match commands::Command::set_commands(&mut self.bot).await {
Err(e) => log::warn!("Failed to set bot commands: {e}"),
_ => log::trace!("Bot commands set successfully!"),
async fn set_commands(&mut self) -> AnyResult<()> {
log::debug!("Setting self commands...");
Command::set_commands(&mut self.bot).await
.context("Aggiornamento dei comandi del bot non riuscito.")
}
log::debug!("Sending start notification...");
match self.send_start_notification(&me).await {
Err(e) => log::warn!("Failed to send start notification: {e}"),
_ => log::trace!("Start notification sent successfully!"),
}
fn dispatcher(&mut self) -> Dispatcher<Bot, AnyError, DefaultKey> {
log::debug!("Building dispatcher...");
log::debug!("Starting Telegram dispatcher...");
dispatcher(self.bot, me).dispatch().await;
log::error!("Telegram dispatcher has exited, bailing out...");
anyhow::bail!("Telegram dispatcher has exited.")
}
}
fn dispatcher(bot: Bot, me: Me) -> Dispatcher<Bot, Error, DefaultKey> {
let bot_name = me.user.username.unwrap();
log::trace!("Bot name is: {bot_name:?}");
let bot_name = self.me.user.username.as_ref().unwrap();
log::trace!("Bot username is: @{bot_name:?}");
log::trace!("Determining pseudo-command regex...");
let regex = Regex::new(&format!(r"^/[a-z0-9_]+(?:@{bot_name})?(?:\s+.*)?$")).unwrap();
log::trace!("Pseudo-command regex is: {regex:?}");
let database = DatabaseInterface::new(self.database_url.clone());
log::trace!("Building dispatcher...");
Dispatcher::builder(
bot,
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))
message
.text()
.is_some_and(|text| regex.is_match(text))
})
// Commands
.branch(
entry()
.filter_command::<commands::Command>()
.endpoint(commands::Command::handle)
// Only process commands matching a valid command, and parse their arguments
.filter_command::<Command>()
// Delegate handling
.endpoint(Command::handle)
)
// No valid command was found
.endpoint(commands::unknown_command)
)
)
.dependencies(
dptree::deps![] // No deps needed at the moment.
dptree::deps![
database
]
)
.build()
}
async fn dispatch(&mut self) -> AnyResult<()> {
log::debug!("Starting Telegram dispatcher...");
self.dispatcher().dispatch().await;
anyhow::bail!("Telegram dispatcher has exited unexpectedly.")
}
}
impl RoyalnetService for TelegramService {
async fn run(&mut self) -> AnyResult<()> {
log::info!("Starting Telegram service...");
let _ = self.set_commands()
.await;
let _ = self.send_start_notification()
.await;
self.dispatch()
.await
}
}

View file

@ -1,5 +0,0 @@
use micronfig::config;
config! {
STRATZ_TOKEN: String,
}

View file

@ -1,86 +0,0 @@
use graphql_client::GraphQLQuery;
use reqwest::Client;
use thiserror::Error;
pub(self) mod config;
const STRATZ_GRAPHQL_API_URL: &str = "https://api.stratz.com/graphql";
// Bind these weird types used in the STRATZ API
type Short = i16;
type Long = i64;
type Byte = u8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GuildId(pub i64);
impl From<i64> for GuildId {
fn from(value: i64) -> Self {
Self(value)
}
}
#[derive(GraphQLQuery)]
#[graphql(schema_path="src/stratz/schema.json", query_path="src/stratz/query_guild_matches.gql", response_derives="Debug, Clone")]
pub struct GuildMatchesQuery;
#[derive(Debug, Clone, Error)]
pub enum QueryError {
#[error("GraphQL request failed")]
Requesting,
#[error("GraphQL response parsing failed")]
Parsing,
}
type GuildMatchesQueryResponse = graphql_client::Response<guild_matches_query::ResponseData>;
#[allow(unused_imports)]
pub use guild_matches_query::LobbyTypeEnum as LobbyType;
#[allow(unused_imports)]
pub use guild_matches_query::GameModeEnumType as GameMode;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuild as Guild;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuildMatches as Match;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuildMatchesPlayers as Player;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuildMatchesPlayersHero as Hero;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuildMatchesPlayersSteamAccount as Steam;
#[allow(unused_imports)]
pub use guild_matches_query::MatchPlayerRoleType as Role;
#[allow(unused_imports)]
pub use guild_matches_query::MatchLaneType as Lane;
#[allow(unused_imports)]
pub use guild_matches_query::GuildMatchesQueryGuildMatchesPlayersStatsMatchPlayerBuffEvent as Buff;
/// Get the latest 10 matches of a certain Dota 2 guild.
pub async fn query_guild_matches(client: &Client, guild_id: &GuildId) -> Result<GuildMatchesQueryResponse, QueryError> {
log::debug!("Querying guild matches with {client:?} for {guild_id:?}...");
log::trace!("Configuring query variables...");
let params = guild_matches_query::Variables {
guild_id: guild_id.0,
};
log::trace!("Building query...");
let body = GuildMatchesQuery::build_query(params);
log::trace!("Building API URL...");
let url = format!("{}?jwt={}", STRATZ_GRAPHQL_API_URL, config::STRATZ_TOKEN());
log::trace!("STRATZ API URL is: {url:?}");
log::trace!("Making request...");
let response = client.post(url)
.json(&body)
.send().await
.map_err(|_| QueryError::Requesting)?
.json::<GuildMatchesQueryResponse>().await
.map_err(|_| QueryError::Parsing)?;
log::trace!("Request successful!");
Ok(response)
}

View file

@ -1,27 +0,0 @@
use teloxide::types::ChatId;
pub struct ChatIdConversionHack(i64);
impl From<i64> for ChatIdConversionHack {
fn from(value: i64) -> Self {
Self(value)
}
}
impl From<ChatIdConversionHack> for ChatId {
fn from(value: ChatIdConversionHack) -> Self {
Self(value.0)
}
}
impl From<ChatIdConversionHack> for i64 {
fn from(value: ChatIdConversionHack) -> Self {
value.0
}
}
impl From<ChatId> for ChatIdConversionHack {
fn from(value: ChatId) -> Self {
Self(value.0)
}
}

View file

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

2
src/utils/result.rs Normal file
View file

@ -0,0 +1,2 @@
pub use anyhow::Result as AnyResult;
pub use anyhow::Error as AnyError;