diff --git a/src/handlers/minecraft.rs b/src/handlers/minecraft.rs new file mode 100644 index 0000000..daf144a --- /dev/null +++ b/src/handlers/minecraft.rs @@ -0,0 +1,108 @@ +use reqwest::{header::HeaderValue, Client}; +use serde::Deserialize; +use url::Url; + +use crate::types::{ServerInfo, ServerOnlineResponse, ServerResponse}; + +#[derive(Deserialize, Debug)] +struct ApiResponse { + online: bool, + version: Option, + players: Option, + motd: Option, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +struct ApiResponsePlayers { + online: u64, + max: u64, +} + +#[derive(Deserialize, Debug)] +struct Motd { + raw: Vec, + clean: Vec, + html: Vec, +} + +#[derive(Debug)] +pub struct OnlineResponse { + searchable_name: String, + reported_name: String, + clean_name: String, + players_online: u64, + player_limit: u64, + version: String, +} + +impl ServerOnlineResponse for OnlineResponse { + fn players_online(&self) -> u64 { + self.players_online + } + + fn player_limit(&self) -> u64 { + self.player_limit + } + + fn version(&self) -> &String { + &self.version + } +} + +pub struct Server { + token: String, + addr: Url, +} + +impl ServerInfo for Server { + fn new(token: String, addr: Url) -> Self { + Server { token, addr } + } + + type OnlineResponse = OnlineResponse; + + type AddressableName = Url; + + fn addressable_name(&self) -> &Self::AddressableName { + &self.addr + } + + fn app_token(&self) -> String { + self.token.clone() + } + + async fn poll(&self) -> ServerResponse { + use reqwest; + let url_string = format!("https://api.mcsrvstat.us/3/{}", self.addr.host().unwrap()); + let url = Url::try_from(url_string.as_str()).unwrap(); + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + request.headers_mut().append("User-Agent", HeaderValue::from_str("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36").unwrap()); + + let client = Client::new(); + + match client.execute(request).await { + Err(error) => ServerResponse::Error(anyhow::anyhow!(error)), + Ok(answer) => { + let json = answer.json::().await.unwrap(); + //println!("{}", answer.text().await.unwrap()); + //ServerResponse::Offline + + match json.online { + true => { + let motd = json.motd.as_ref().unwrap(); + self::ServerResponse::Online(OnlineResponse { + searchable_name: motd.clean.get(0).unwrap().into(), + reported_name: motd.clean.get(0).unwrap().into(), + clean_name: motd.clean.get(0).unwrap().into(), + players_online: json.players.unwrap().online, + player_limit: json.players.unwrap().max, + version: json.version.unwrap(), + }) + } + false => ServerResponse::Offline, + } + + } + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..b758303 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod minecraft; +pub mod scpsl; diff --git a/src/handlers/scpsl.rs b/src/handlers/scpsl.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/handlers/scpsl.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs index a486946..b092fb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,17 @@ -mod funcs; -mod minecraft; -mod scpsl; +mod handlers; mod types; -use dotenvy::{self, dotenv}; -use minecraft::Minecraft; -use tokio::join; -use url::{self, Url}; -#[macro_use] -extern crate log; +use crate::{handlers::minecraft, types::ServerInfo}; + +use tokio; #[tokio::main] async fn main() { - dotenv().ok(); - pretty_env_logger::init(); - let gamerzone = Minecraft::new( - Url::try_from("https://api.mcstatus.io/v2/status/java/bleat.shibedrill.site").unwrap(), - std::env::var("TOKEN_BOT_MC_BLEAT").unwrap(), - "DeersCord SMP".into(), + println!("Hello world!"); + let mc = minecraft::Server::new( + "foo".into(), + "http://dawn.shibedrill.site".try_into().unwrap(), ); - let mchprs = Minecraft::new( - Url::try_from("https://api.mcstatus.io/v2/status/java/mchprs.shibedrill.site").unwrap(), - std::env::var("TOKEN_BOT_MC_MCHPRS").unwrap(), - "Project MCRV".into(), - ); - let dawn = Minecraft::new( - Url::try_from("https://api.mcstatus.io/v2/status/java/dawn.shibedrill.site").unwrap(), - std::env::var("TOKEN_BOT_MC_DAWN").unwrap(), - "Dawn Group".into(), - ); - join!(mchprs.run(), gamerzone.run(), dawn.run()); + let results = mc.poll().await; + println!("{:#?}", results); } diff --git a/src/minecraft.rs b/src/minecraft.rs deleted file mode 100644 index 0864459..0000000 --- a/src/minecraft.rs +++ /dev/null @@ -1,131 +0,0 @@ -use poise::serenity_prelude as serenity; -use reqwest::{Client, Request}; -use serenity::*; -use url::Url; - -use crate::{ - funcs, - types::{self, ServerResponse}, -}; - -#[derive(serde::Deserialize, Debug)] -struct ServerSummary { - online: bool, - players: Option, - version: Version, -} - -#[derive(serde::Deserialize, Debug)] -struct Version { - name_raw: String, - name_clean: String, - name_html: String, - protocol: u32, -} - -#[derive(serde::Deserialize, Debug)] -struct Players { - online: i32, - max: i32, -} - -pub struct Data { - controller: Minecraft, -} - -#[derive(Clone)] -pub struct Minecraft { - url: Url, - token: String, - name: String, -} - -impl Minecraft { - pub fn new(url: Url, token: String, name: String) -> Self { - Self { url, token, name } - } - - pub async fn get_status(&self) -> Result { - let http_client = Client::new(); - trace!("Created HTTP client"); - let request = Request::new(reqwest::Method::GET, self.url.clone()); - trace!("Created HTTP request"); - let response = http_client.execute(request).await?; - trace!("Ran request using client"); - let data: ServerSummary = serde_json::from_str(&response.text().await?)?; - - trace!("Response JSON for {}: {:#?}", self.name, data); // this - - if let Some(players) = data.players { - Ok(ServerResponse::new( - data.online, - Some(players.online as u32), - Some(players.max as u32), - Some(data.version.name_raw), - )) - } else { - Ok(ServerResponse::new(data.online, None, None, None)) - } - } - pub async fn run(&self) { - let controller = self.clone(); - let framework = poise::Framework::builder() - .options(poise::FrameworkOptions { - event_handler: |ctx, event, framework, data| { - Box::pin(event_handler(ctx, event, framework, data)) - }, - ..Default::default() - }) - .setup(|ctx, _ready, framework| { - Box::pin(async move { - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data { controller }) - }) - }) - .build(); - info!("Built framework successfully."); - - let mut discord_client = ClientBuilder::new( - self.token.clone(), - serenity::GatewayIntents::non_privileged(), - ) - .framework(framework) - .activity(ActivityData::custom("Waiting on initial status...")) - .await - .inspect_err(|e| error!("Failed to start client: {}", e)) - .unwrap(); - info!("Built client successfully."); - - let _ = discord_client.start().await; - } -} - -pub async fn event_handler( - ctx: &serenity::Context, - event: &serenity::FullEvent, - _framework: poise::FrameworkContext<'_, Data, Error>, - data: &Data, -) -> Result<(), Error> { - match event { - serenity::FullEvent::Ready { - data_about_bot: _bot_data, - } => loop { - trace!("Running loop routine"); - let status = data - .controller - .get_status() - .await - .inspect_err(|e| error!("{}: Failed to get status: {}", data.controller.name, e)) - .unwrap(); - info!( - "{}: Got status: {}", - data.controller.name, - status.to_string() - ); - - funcs::set_presence(ctx, status); - tokio::time::sleep(std::time::Duration::from_secs(30)).await; - }, - _ => Ok(()), - } -} diff --git a/src/scpsl.rs b/src/scpsl.rs deleted file mode 100644 index f310d10..0000000 --- a/src/scpsl.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::{funcs, types::ServerResponse}; -use poise::serenity_prelude as serenity; -use reqwest::{Client, Request}; -use serenity::*; -use url::Url; - -#[derive(serde::Deserialize, Debug)] -#[allow(non_snake_case)] -struct ServerSummary { - online: bool, - players: String, -} - -pub struct Data { - controller: Scpsl, -} - -#[derive(Clone)] -pub struct Scpsl { - name: String, - url: Url, - token: String, -} - -impl Scpsl { - pub fn new(url: Url, token: String, name: String) -> Self { - Self { url, token, name } - } - async fn get_status(&self) -> Result { - let http_client = Client::new(); - trace!("Created HTTP client"); - let request = Request::new(reqwest::Method::GET, self.url.clone()); - trace!("Created HTTP request"); - let response = http_client.execute(request).await?; - trace!("Ran request using client"); - let data: ServerSummary = serde_json::from_str(&response.text().await?)?; - - trace!("Response JSON for {}: {:#?}", self.name, data); // this is the only thing I added. - - let playercount: Result, _> = - data.players.split('/').map(|x| x.parse::()).collect(); - - let playercount_unwrapped = playercount?; - - Ok(ServerResponse::new( - data.online, - playercount_unwrapped.first().copied(), - playercount_unwrapped.get(1).copied(), - None, - )) - } - - pub async fn run(&self) { - let controller = self.clone(); - let framework = poise::Framework::builder() - .options(poise::FrameworkOptions { - initialize_owners: true, - event_handler: |ctx, event, framework, data| { - Box::pin(event_handler(ctx, event, framework, data)) - }, - ..Default::default() - }) - .setup(|ctx, _ready, framework| { - Box::pin(async move { - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data { controller }) - }) - }) - .build(); - info!("Built framework successfully."); - - let mut discord_client = ClientBuilder::new( - self.token.clone(), - serenity::GatewayIntents::non_privileged(), - ) - .framework(framework) - .activity(ActivityData::custom("Waiting on initial status...")) - .await - .inspect_err(|e| error!("Failed to start client: {}", e)) - .unwrap(); - info!("Built client successfully."); - - let _ = discord_client.start().await; - } -} - -pub async fn event_handler( - ctx: &serenity::Context, - event: &serenity::FullEvent, - _framework: poise::FrameworkContext<'_, Data, Error>, - data: &Data, -) -> Result<(), Error> { - match event { - serenity::FullEvent::Ready { - data_about_bot: _data, - } => loop { - trace!("Running loop routine"); - let status = data - .controller - .get_status() - .await - .inspect_err(|e| error!("{}: Failed to get status: {}", data.controller.name, e)) - .unwrap(); - info!( - "{}: Got status: {}", - data.controller.name, - status.to_string() - ); - - funcs::set_presence(ctx, status); - tokio::time::sleep(std::time::Duration::from_secs(30)).await; - }, - _ => Ok(()), - } -} diff --git a/src/types.rs b/src/types.rs index d7f1be6..ba1b87e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,65 +1,31 @@ -use std::fmt::format; +use std::net::IpAddr; +use url::Url; -pub struct ServerResponse { - online: bool, - players: Option, - max: Option, - version: Option, +#[derive(Debug)] +pub enum ServerResponse { + Offline, + Online(T), + Error(anyhow::Error), } -impl ServerResponse { - pub fn new( - online: bool, - players: Option, - max: Option, - version: Option, - ) -> Self { - ServerResponse { - online, - players, - max, - version, - } - } - - pub fn online(&self) -> bool { - self.online - } - - pub fn players(&self) -> Option { - self.players - } - - pub fn max(&self) -> Option { - self.max - } - - pub fn is_full(&self) -> bool { - self.players >= self.max - } - - pub fn version(&self) -> &Option { - &self.version - } - - pub fn to_string(&self) -> String { - format!( - "{} {} {}", - if let Some(players) = self.players { - if let Some(max) = self.max { - format!("{}/{}", players, max) - } else { - players.to_string() - } - } else { - "N/A".into() - }, - self.online, - if let Some(version) = &self.version { - version.to_string() - } else { - "N/A".into() - } - ) - } +pub trait ServerOnlineResponse { + fn players_online(&self) -> u64; + fn player_limit(&self) -> u64; + fn version(&self) -> &String; +} + +pub trait ServerInfo { + fn new(token: String, addr: Url) -> Self; + + /// An addressable, unique identifier, used for its API. + /// If it is a Minecraft server, this can be a fully qualified domain name or URL. + /// If this is an SCP secret laboratory server, it can be a server ID. + type AddressableName; + fn addressable_name(&self) -> &Self::AddressableName; + + /// The app token used to send status updates. + fn app_token(&self) -> String; + + type OnlineResponse: ServerOnlineResponse; + async fn poll(&self) -> ServerResponse; }