diff --git a/.gitignore b/.gitignore index 0b745e2..8468e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.env \ No newline at end of file +.env +config.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d24f074..b865750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,12 +492,13 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -506,9 +507,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -516,9 +517,20 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" @@ -528,9 +540,9 @@ checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -539,21 +551,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -563,7 +575,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1189,17 +1200,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "playerbot" -version = "0.6.1" +version = "1.0.0" dependencies = [ "anyhow", + "futures", "poise", "reqwest 0.13.3", "serde", diff --git a/Cargo.toml b/Cargo.toml index c0ff3a8..d9f2eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "playerbot" -version = "0.6.1" +version = "1.0.0" edition = "2021" [dependencies] anyhow = "1.0.102" +futures = "0.3.32" poise = "0.6.2" reqwest = {version = "0.13.3", features = ["json"]} serde = {version = "1.0.215", features = ["derive", "serde_derive"]} diff --git a/config.json b/config.json index a7d93b5..c7540c4 100644 --- a/config.json +++ b/config.json @@ -4,7 +4,7 @@ { "handler_type": "minecraft", "address": "https://dawn.shibedrill.site", - "token": "foobar" + "token": "MTI5Nzc1NjA3ODE2MjgzNzU0NA.GGjPC_.oy075omzc4IxkX84xc_H7qoYrtil7T0d7ampJ0" } ] } \ No newline at end of file diff --git a/src/bot_runner.rs b/src/bot_runner.rs index 2043819..095d82b 100644 --- a/src/bot_runner.rs +++ b/src/bot_runner.rs @@ -1,7 +1,135 @@ -use crate::types::*; -use poise; -struct BotRunner { + +use crate::request::request; +use crate::types::*; +use poise::serenity_prelude as serenity; +use poise::serenity_prelude::{ActivityData, ClientBuilder, GatewayIntents}; +use tokio::sync::Mutex; + +pub struct BotRunner { client: poise::serenity_prelude::Client, - server_data: dyn ServerInfo, } + +pub struct Data { + server: Box, + cached_reply: Mutex>, +} + +impl BotRunner { + pub async fn new(server: Box) -> Self { + let token = server.app_token(); + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: vec![players(), join()], + 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 { + server, + cached_reply: Mutex::new(None), + }) + }) + }) + .build(); + + let client = ClientBuilder::new(token, GatewayIntents::non_privileged()) + .framework(framework) + .activity(ActivityData::custom("Waiting on first heartbeat")) + .await + .unwrap(); + BotRunner { client } + } + + pub async fn run(&mut self) -> Result<(), serenity::Error> { + self.client.start().await + } +} + +pub async fn event_handler( + ctx: &serenity::Context, + event: &serenity::FullEvent, + _framework: poise::FrameworkContext<'_, Data, serenity::Error>, + data: &Data, +) -> Result<(), serenity::Error> { + match event { + serenity::FullEvent::Ready { + data_about_bot: _bot_data, + } => loop { + println!("Checking status: {}", data.server.api_address()); + let http_response = request(data.server.api_address()).await.unwrap(); + let results = data.server.parse(http_response.text().await.unwrap()); + match &results { + ServerResponse::Offline => { + ctx.set_presence( + Some(ActivityData::custom("Server offline!")), + serenity::OnlineStatus::DoNotDisturb, + ); + } + ServerResponse::Online(online_info) => { + ctx.set_presence( + Some(ActivityData::custom(format!( + "{}/{} online, v{}", + online_info.players_online, + online_info.player_limit, + online_info.version + ))), + if online_info.players_online > 0 { + serenity::OnlineStatus::Online + } else { + serenity::OnlineStatus::Idle + }, + ); + } + } + //println!("{:#?}", &results); + *data.cached_reply.lock().await = Some(results); + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + }, + _ => Ok(()), + } +} + +#[poise::command(slash_command)] +pub async fn players( + ctx: poise::Context<'_, Data, serenity::Error>, +) -> Result<(), serenity::Error> { + if let Some(reply) = &*ctx.data().cached_reply.lock().await { + match reply { + ServerResponse::Offline => ctx.say("Server is offline!").await?, + ServerResponse::Online(on) => { + if let Some(list) = &on.players { + ctx.say(format!( + "{} out of {} players are online.\nPlayers online: {}", + on.players_online, + on.player_limit, + list.join(", ") + )) + .await? + } else { + ctx.say(format!( + "{} out of {} players are online.", + on.players_online, on.player_limit + )) + .await? + } + } + }; + } else { + ctx.say("No status response yet. Please wait 30 seconds.") + .await?; + } + Ok(()) +} + +#[poise::command(slash_command)] +pub async fn join( + ctx: poise::Context<'_, Data, serenity::Error>, +) -> Result<(), serenity::Error> { + ctx.say(format!("To join, type `{}` in the server URL box or search bar.", ctx.data().server.addressable_name())).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/config_parser.rs b/src/config_parser.rs index 3d2d89d..267bcfb 100644 --- a/src/config_parser.rs +++ b/src/config_parser.rs @@ -1,6 +1,6 @@ -use std::{fs::File, path::Path}; use crate::{handlers, types::ServerInfo}; use serde::Deserialize; +use std::{fs::File, path::Path}; use url::Url; #[derive(Deserialize)] @@ -22,14 +22,16 @@ pub fn parse_configs(path: &Path) -> Result { } pub fn build_handlers(conf: Config) -> Vec> { - let mut results: Vec> = vec!(); + assert_eq!(conf.version, "0.1.0"); + let mut results: Vec> = vec![]; for item in conf.entries { match item.handler_type.as_str() { - "minecraft" => { - results.push(Box::new(handlers::minecraft::Server::new(item.token, item.address))) - }, - _ => {} + "minecraft" => results.push(Box::new(handlers::minecraft::Server::new( + item.token, + item.address, + ))), + _ => {} } } results -} \ No newline at end of file +} diff --git a/src/handlers/minecraft.rs b/src/handlers/minecraft.rs index c91e9cb..f490073 100644 --- a/src/handlers/minecraft.rs +++ b/src/handlers/minecraft.rs @@ -84,15 +84,21 @@ impl ServerInfo for Server { players_online: players.online, player_limit: players.max, version: parsed_data.version.unwrap(), - players: Some(players - .list - .unwrap_or_default() - .iter() - .map(|e| e.name.clone()) - .collect()), + players: Some( + players + .list + .unwrap_or_default() + .iter() + .map(|e| e.name.clone()) + .collect(), + ), }) } false => ServerResponse::Offline, } } + + fn supports_playerlist(&self) -> bool { + true + } } diff --git a/src/main.rs b/src/main.rs index 9a467c0..e4e8a47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,22 @@ +#![feature(impl_trait_in_bindings)] mod bot_runner; mod config_parser; mod handlers; -mod types; mod request; +mod types; +use crate::bot_runner::BotRunner; +use futures::{self, future::try_join_all}; use std::path::Path; -use crate::{ - request::request, types::ServerResponse -}; - #[tokio::main] async fn main() { - let config = config_parser::parse_configs(Path::new("config.json")).unwrap(); let handlers = config_parser::build_handlers(config); - - let http_response = request(handlers[0].api_address()).await.unwrap(); - let results = handlers[0].parse(http_response.text().await.unwrap()); - match results { - ServerResponse::Offline => println!("Offline"), - ServerResponse::Online(online_info) => { - println!( - "Name: {}\nAddress: {}\nVersion: {}\nPlayers: {}/{}", - online_info.readable_name, - online_info.searchable_name, - online_info.version, - online_info.players_online, - online_info.player_limit - ); - println!("Players online: {}", online_info.players.unwrap().join(", ")); - } + let mut bots: Vec = vec![]; + for item in handlers { + bots.push(BotRunner::new(item).await); } + let futures: Vec<_> = bots.iter_mut().map(|b| b.run()).collect(); + let _ = try_join_all(futures).await; } diff --git a/src/request.rs b/src/request.rs index 18e9a54..9874640 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,9 +1,9 @@ +use reqwest::{header::HeaderValue, Response}; use url::Url; -use reqwest::{Response, header::HeaderValue}; pub async fn request(url: Url) -> Result { let client = reqwest::Client::new(); 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()); client.execute(request).await -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs index 77d960c..949a185 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,6 +7,7 @@ pub enum ServerResponse { } #[derive(Debug)] +#[allow(dead_code)] pub struct ServerOnlineResponse { pub players_online: u64, pub player_limit: u64, @@ -19,7 +20,7 @@ pub struct ServerOnlineResponse { } #[allow(dead_code)] -pub trait ServerInfo { +pub trait ServerInfo: Send + Sync { fn new(token: String, addr: Url) -> Self where Self: Sized; @@ -35,4 +36,5 @@ pub trait ServerInfo { /// The app token used to send status updates. fn app_token(&self) -> String; + fn supports_playerlist(&self) -> bool; }