pub mod common; use dotenvy::dotenv; use serde::{Deserialize, Serialize}; use serenity::{ model::id::{GuildId, RoleId}, prelude::TypeMapKey, }; use crate::set_roles::get_server_member_bulk; use chrono::{Datelike, SecondsFormat, Utc}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::de::DeserializeOwned; use serenity::client::Context; use serenity::model::id::UserId; use serenity::model::prelude::application_command::ApplicationCommandInteraction; use sqlx::{ Pool, Sqlite, }; use std::{env, sync::Arc}; use tokio::sync::RwLock; use common::database::{Minecraft, ServerMembersWolves, Servers}; pub struct Config { // manages where teh database is stored pub home: String, pub database: String, // tokens for discord and other API's pub discord_token: String, pub discord_token_minecraft: String, // email settings pub mail_smtp: String, pub mail_user: String, pub mail_pass: String, // wolves API base for clubs/socs pub wolves_url: String, } impl TypeMapKey for Config { type Value = Arc>; } pub fn get_config() -> Config { dotenv().ok(); // reasonable defaults let mut config = Config { discord_token: "".to_string(), discord_token_minecraft: "".to_string(), home: ".".to_string(), database: "database.db".to_string(), mail_smtp: "".to_string(), mail_user: "".to_string(), mail_pass: "".to_string(), wolves_url: "".to_string(), }; if let Ok(x) = env::var("DATABASE_HOME") { config.home = x.trim().to_string(); } if let Ok(x) = env::var("DATABASE") { config.database = x.trim().to_string(); } if let Ok(x) = env::var("DISCORD_TOKEN") { config.discord_token = x.trim().to_string(); } if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") { config.discord_token_minecraft = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_SMTP") { config.mail_smtp = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_USER") { config.mail_user = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_PASS") { config.mail_pass = x.trim().to_string(); } if let Ok(x) = env::var("WOLVES_URL") { config.wolves_url = x.trim().to_string(); } config } pub fn get_now_iso(short: bool) -> String { let now = Utc::now(); if short { format!("{}-{:02}-{:02}", now.year(), now.month(), now.day()) } else { now.to_rfc3339_opts(SecondsFormat::Millis, true) } } pub fn random_string(len: usize) -> String { thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() } pub mod set_roles { use crate::common::database::{DataBase, Wolves}; use super::*; pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option], members_changed: &[UserId]) { let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Database in TypeMap.").clone() }; let db = db_lock.read().await; let Servers { server, role_past, role_current, .. } = server; let mut roles_set = [0, 0, 0]; let mut members = vec![]; for member in get_server_member_bulk(&db, server).await { if let Some(x) = member.discord { members.push(x); } } let mut members_all = members.len(); if let Ok(x) = server.members(ctx, None, None).await { for mut member in x { // members_changed acts as an override to only deal with teh users in it if !members_changed.is_empty() && !members_changed.contains(&member.user.id) { continue; } if members.contains(&member.user.id) { let mut roles = vec![]; if let Some(role) = &role_past { if !member.roles.contains(role) { roles_set[0] += 1; roles.push(role.to_owned()); } } if !member.roles.contains(role_current) { roles_set[1] += 1; roles.push(role_current.to_owned()); } if let Err(e) = member.add_roles(ctx, &roles).await { println!("{:?}", e); } } else { // old and never if let Some(role) = &role_past { if member.roles.contains(role) { members_all += 1; } } if member.roles.contains(role_current) { roles_set[2] += 1; // if theya re not a current member and have the role then remove it if let Err(e) = member.remove_role(ctx, role_current).await { println!("{:?}", e); } } } for role in remove_roles.iter().flatten() { if let Err(e) = member.remove_role(ctx, role).await { println!("{:?}", e); } } } } set_server_numbers(&db, server, members_all as i64, members.len() as i64).await; // small bit of logging to note changes over time println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]); } pub async fn get_server_member_bulk(db: &Pool, server: &GuildId) -> Vec { sqlx::query_as::<_, ServerMembersWolves>( r#" SELECT * FROM server_members JOIN wolves USING (id_wolves) WHERE ( server = ? AND discord IS NOT NULL AND expiry > ? ) "#, ) .bind(*server.as_u64() as i64) .bind(get_now_iso(true)) .fetch_all(db) .await .unwrap_or_default() } async fn set_server_numbers(db: &Pool, server: &GuildId, past: i64, current: i64) { match sqlx::query_as::<_, Wolves>( " UPDATE servers SET member_past = ?, member_current = ? WHERE server = ? ", ) .bind(past) .bind(current) .bind(*server.as_u64() as i64) .fetch_optional(db) .await { Ok(_) => {} Err(e) => { println!("Failure to insert into {}", server.as_u64()); println!("{:?}", e); } } } } /** For any time ye need to check if a user who calls a command has admin privlages */ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) -> Option { let mut admin = false; let g_id = match command.guild_id { None => return Some("Not in a server".to_string()), Some(x) => x, }; let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default(); if let Ok(member) = g_id.member(&ctx.http, command.user.id).await { if let Some(permissions) = member.permissions { if permissions.administrator() { admin = true; } } for role_id in member.roles { if admin { break; } if let Some(role) = roles_server.get(&role_id) { if role.permissions.administrator() { admin = true; } } } } if !admin { Some("Administrator permission required".to_string()) } else { None } } /** loop through all members of server get a list of folks with mc accounts that are members and a list that arent members */ pub async fn update_server(server_id: &str, db: &Pool, g_id: &GuildId, config: &Config) { let mut usernames = vec![]; for member in get_server_member_bulk(db, g_id).await { if let Some(x) = member.minecraft { usernames.push(x); } } if !usernames.is_empty() { whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await; } } async fn post(url: &str, bearer: &str, data: &T) { match surf::post(url) .header("Authorization", bearer) .header("Content-Type", "application/json") .header("Accept", "Application/vnd.pterodactyl.v1+json") .body_json(&data) { Ok(req) => { req.await.ok(); } Err(e) => { dbg!(e); } } } #[derive(Deserialize, Serialize, Debug)] pub struct ServerDetailsResSub { pub identifier: String, pub name: String, pub description: String, pub is_suspended: bool, } #[derive(Deserialize, Serialize, Debug)] pub struct ServerDetailsRes { pub attributes: ServerDetailsResSub, } async fn get(url: &str, bearer: &str) -> Option { match surf::get(url) .header("Authorization", bearer) .header("Content-Type", "application/json") .header("Accept", "Application/vnd.pterodactyl.v1+json") .recv_json() .await { Ok(res) => Some(res), Err(e) => { dbg!(e); None } } } #[derive(Deserialize, Serialize, Debug)] struct BodyCommand { command: String, } #[derive(Deserialize, Serialize, Debug)] struct BodyDelete { root: String, files: Vec, } pub async fn whitelist_update(add: &Vec, server: &str, token: &str) { let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); let bearer = format!("Bearer {token}"); for name in add { let data = BodyCommand { command: format!("whitelist add {name}"), }; post(&format!("{url_base}/command"), &bearer, &data).await; } } pub async fn whitelist_wipe(server: &str, token: &str) { let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); let bearer = format!("Bearer {token}"); // delete whitelist let deletion = BodyDelete { root: "/".to_string(), files: vec!["whitelist.json".to_string()], }; post(&format!("{url_base}/files/delete"), &bearer, &deletion).await; // recreate teh file, passing in the type here so the compiler knows what type of vec it is post::>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await; // reload the whitelist let data = BodyCommand { command: "whitelist reload".to_string(), }; post(&format!("{url_base}/command"), &bearer, &data).await; } pub async fn server_information(server: &str, token: &str) -> Option { let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); let bearer = format!("Bearer {token}"); get::(&format!("{url_base}/"), &bearer).await } pub async fn get_minecraft_config(db: &Pool) -> Vec { sqlx::query_as::<_, Minecraft>( r#" SELECT * FROM minecraft "#, ) .fetch_all(db) .await .unwrap_or_default() } pub async fn get_minecraft_config_server(db: &Pool, g_id: GuildId) -> Vec { sqlx::query_as::<_, Minecraft>( r#" SELECT * FROM minecraft WHERE server_discord = ?1 "#, ) .bind(*g_id.as_u64() as i64) .fetch_all(db) .await .unwrap_or_default() }