Merge pull request '#17-automate-onboarding-mk-ii' (#30) from #17-automate-onboarding-mk-ii into main
All checks were successful
On_Push / lint_fmt (push) Successful in 14s
On_Push / lint_clippy (push) Successful in 6m46s
On_Push / build (push) Successful in 12m25s
On_Push / deploy (push) Successful in 24s

Reviewed-on: #30
This commit is contained in:
silver 2025-02-19 10:30:54 +00:00
commit 25fcc04287
20 changed files with 2131 additions and 1754 deletions

956
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,28 +16,30 @@ name = "update_minecraft"
[dependencies]
# discord library
serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "full"] }
# wolves api
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git" }
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"] }
# wolves_oxidised = { path = "../wolves-oxidised", features = ["unstable"] }
# to make the http requests
surf = "2.3.2"
surf = "2.3"
dotenvy = "0.15.7"
dotenvy = "0.15"
# For sqlite
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "migrate" ] }
serde_json = { version = "1.0", features = ["raw_value"] }
# create random strings
rand = "0.8.5"
rand = "0.9"
# fancy time stuff
chrono = "0.4.26"
chrono = "0.4"
# for email
lettre = "0.10.4"
maud = "0.25.0"
lettre = "0.11"
maud = "0.27"
serde = "1.0"

View file

@ -0,0 +1,14 @@
-- No longer using the previous committee table
DROP TABLE committee;
-- new table pulling from teh api
CREATE TABLE IF NOT EXISTS committees (
id integer PRIMARY KEY,
name_profile text not null,
name_plain text not null,
name_full text not null,
link text not null,
committee text not null
);
ALTER TABLE servers DROP COLUMN wolves_link;

View file

@ -46,10 +46,6 @@ You (personally) will need a role with ``Administrator`` permission to be able t
4. ``role_past`` (optional) is the role for all current and past members.
5. ``bot_channel`` is a channel that folks are recommended to use the bot.
* You can have it so folks cannot see message history
6. ``server_name`` For example ``UL Computer Society``
* Will be removed in the future
7. ``wolves_link`` for example <https://ulwolves.ie/society/computer>
* Will be removed in the future
At this point the bot is set up and no further action is required.

View file

@ -4,7 +4,10 @@ use serenity::{
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::{db_init, get_config, get_data::get_wolves, Config, DataBase};
use skynet_discord_bot::common::database::{db_init, DataBase};
use skynet_discord_bot::common::wolves::cns::get_wolves;
use skynet_discord_bot::common::wolves::committees::get_cns;
use skynet_discord_bot::{get_config, Config};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -13,7 +16,10 @@ async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(_) => return,
Err(e) => {
dbg!(e);
return;
}
};
// Intents are a bitflag, bitwise operations can be used to dictate which intents to use
@ -43,8 +49,12 @@ impl EventHandler for Handler {
let ctx = Arc::new(ctx);
println!("{} is connected!", ready.user.name);
// get the data for each individual club/soc
get_wolves(&ctx).await;
// get teh data for the clubs/socs committees
get_cns(&ctx).await;
// finish up
process::exit(0);
}

View file

@ -1,4 +1,6 @@
use skynet_discord_bot::{db_init, get_config, get_minecraft_config, update_server, whitelist_wipe};
use skynet_discord_bot::common::database::db_init;
use skynet_discord_bot::common::minecraft::{get_minecraft_config, update_server, whitelist_wipe};
use skynet_discord_bot::get_config;
use std::collections::HashSet;
#[tokio::main]

View file

@ -4,7 +4,9 @@ use serenity::{
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles, Config, DataBase};
use skynet_discord_bot::common::database::{db_init, get_server_config_bulk, DataBase};
use skynet_discord_bot::common::set_roles::{committee, normal};
use skynet_discord_bot::{get_config, Config};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -43,14 +45,18 @@ impl EventHandler for Handler {
let ctx = Arc::new(ctx);
println!("{} is connected!", ready.user.name);
bulk_check(Arc::clone(&ctx)).await;
// this goes into each server and sets roles for each wolves member
check_bulk(Arc::clone(&ctx)).await;
// u[date committee server
committee::check_committee(Arc::clone(&ctx)).await;
// finish up
process::exit(0);
}
}
async fn bulk_check(ctx: Arc<Context>) {
async fn check_bulk(ctx: Arc<Context>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
@ -59,6 +65,6 @@ async fn bulk_check(ctx: Arc<Context>) {
let db = db_lock.read().await;
for server_config in get_server_config_bulk(&db).await {
set_roles::update_server(&ctx, &server_config, &[], &[]).await;
normal::update_server(&ctx, &server_config, &[], &[]).await;
}
}

View file

@ -1,98 +1,57 @@
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::get_data::get_wolves;
use skynet_discord_bot::{get_server_config, is_admin, set_roles::update_server, DataBase, Servers};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::{builder::CreateCommand, client::Context};
use skynet_discord_bot::common::database::{get_server_config, DataBase, Servers};
use skynet_discord_bot::common::set_roles::normal::update_server;
use skynet_discord_bot::common::wolves::cns::get_wolves;
use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let api_key = if let CommandDataOptionValue::String(key) = command
.data
.options
.first()
.expect("Expected user option")
.resolved
.as_ref()
.expect("Expected user object")
let wolves_api = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(key),
..
}) = command.data.options.first()
{
key
key.to_string()
} else {
return "Please provide a wolves API key".to_string();
};
let role_current = if let CommandDataOptionValue::Role(role) = command
.data
.options
.get(1)
.expect("Expected role option")
.resolved
.as_ref()
.expect("Expected role object")
let role_current = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(1)
{
role.id.to_owned()
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let mut role_past = None;
if let Some(x) = command.data.options.get(5) {
if let Some(CommandDataOptionValue::Role(role)) = &x.resolved {
role_past = Some(role.id.to_owned());
}
let role_past = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(5)
{
Some(role.to_owned())
} else {
None
};
let bot_channel_id = if let CommandDataOptionValue::Channel(channel) = command
.data
.options
.get(2)
.expect("Expected channel option")
.resolved
.as_ref()
.expect("Expected channel object")
let bot_channel_id = if let Some(CommandDataOption {
value: CommandDataOptionValue::Channel(channel),
..
}) = command.data.options.get(2)
{
channel.id.to_owned()
channel.to_owned()
} else {
return "Please provide a valid channel for ``Bot Channel``".to_string();
};
let server_name = if let CommandDataOptionValue::String(name) = command
.data
.options
.get(3)
.expect("Expected Server Name option")
.resolved
.as_ref()
.expect("Expected Server Name object")
{
name
} else {
&"UL Computer Society".to_string()
};
let wolves_link = if let CommandDataOptionValue::String(wolves) = command
.data
.options
.get(4)
.expect("Expected Wolves Link option")
.resolved
.as_ref()
.expect("Expected Server Name object")
{
wolves
} else {
&"https://ulwolves.ie/society/computer".to_string()
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
@ -101,14 +60,13 @@ pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> Stri
let server_data = Servers {
server: command.guild_id.unwrap_or_default(),
wolves_api: api_key.to_owned(),
wolves_api,
role_past,
role_current,
member_past: 0,
member_current: 0,
bot_channel_id,
server_name: server_name.to_owned(),
wolves_link: wolves_link.to_string(),
server_name: "".to_string(),
};
match add_server(&db, ctx, &server_data).await {
@ -122,71 +80,30 @@ pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> Stri
"Added/Updated server info".to_string()
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("add")
pub fn register() -> CreateCommand {
CreateCommand::new("add")
.description("Enable the bot for this discord")
.create_option(|option| {
option
.name("api_key")
.description("UL Wolves API Key")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("role_current")
.description("Role for Current members")
.kind(CommandOptionType::Role)
.required(true)
})
.create_option(|option| {
option
.name("bot_channel")
.description("Safe space for folks to use the bot commands.")
.kind(CommandOptionType::Channel)
.required(true)
})
.create_option(|option| {
option
.name("server_name")
.description("Name of the Discord Server.")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("wolves_link")
.description("Link to the Club/Society on UL Wolves.")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("role_past")
.description("Role for Past members")
.kind(CommandOptionType::Role)
.required(false)
})
.add_option(CreateCommandOption::new(CommandOptionType::String, "api_key", "UL Wolves API Key").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_current", "Role for Current members").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Channel, "bot_channel", "Safe space for folks to use the bot commands.").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_past", "Role for Past members").required(false))
}
async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Result<Option<Servers>, Error> {
let existing = get_server_config(db, &server.server).await;
let role_past = server.role_past.map(|x| *x.as_u64() as i64);
let role_past = server.role_past.map(|x| x.get() as i64);
let insert = sqlx::query_as::<_, Servers>(
"
INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current, bot_channel_id, server_name, wolves_link)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current, bot_channel_id)
VALUES (?1, ?2, ?3, ?4, ?5)
",
)
.bind(*server.server.as_u64() as i64)
.bind(server.server.get() as i64)
.bind(&server.wolves_api)
.bind(role_past)
.bind(*server.role_current.as_u64() as i64)
.bind(*server.bot_channel_id.as_u64() as i64)
.bind(&server.server_name)
.bind(&server.wolves_link)
.bind(server.role_current.get() as i64)
.bind(server.bot_channel_id.get() as i64)
.fetch_optional(db)
.await;

View file

@ -1,311 +0,0 @@
use lettre::{
message::{header, MultiPart, SinglePart},
transport::smtp::{self, authentication::Credentials},
Message, SmtpTransport, Transport,
};
use maud::html;
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
id::UserId,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::{random_string, Config, DataBase};
use sqlx::{Pool, Sqlite};
pub mod link {
use super::*;
use serenity::model::id::GuildId;
use skynet_discord_bot::Committee;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let committee_server = GuildId(1220150752656363520);
match command.guild_id {
None => {
return "Not in correct discord server.".to_string();
}
Some(x) => {
if x != committee_server {
return "Not in correct discord server.".to_string();
}
}
}
let option = command
.data
.options
.first()
.expect("Expected email option")
.resolved
.as_ref()
.expect("Expected email object");
let email = if let CommandDataOptionValue::String(email) = option {
email.trim()
} else {
return "Please provide a valid committee email.".to_string();
};
// fail early
if !email.ends_with("@ulwolves.ie") {
return "Please use a @ulwolves.ie address you have access to.".to_string();
}
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
if get_server_member_discord(&db, &command.user.id).await.is_some() {
return "Already linked".to_string();
}
if get_verify_from_db(&db, &command.user.id).await.is_some() {
return "Linking already in process, please check email.".to_string();
}
// generate a auth key
let auth = random_string(20);
match send_mail(&config, email, &auth, &command.user.name) {
Ok(_) => match save_to_db(&db, email, &auth, &command.user.id).await {
Ok(_) => {}
Err(e) => {
return format!("Unable to save to db {} {e:?}", email);
}
},
Err(e) => {
return format!("Unable to send mail to {} {e:?}", email);
}
}
format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email)
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link_committee")
.description("Verify you are a committee member")
.create_option(|option| {
option
.name("email")
.description("UL Wolves Committee Email")
.kind(CommandOptionType::String)
.required(true)
})
}
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Committee> {
sqlx::query_as::<_, Committee>(
r#"
SELECT *
FROM committee
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result<smtp::response::Response, smtp::Error> {
let sender = format!("UL Computer Society <{}>", &config.mail_user);
// Create the html we want to send.
let html = html! {
head {
title { "Hello from Skynet!" }
style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient.
p { "Hi " (user) "," }
p {
"Please use " pre { "/verify_committee code: " (auth)} " to verify your discord account."
}
p {
"Skynet Team"
br;
"UL Computer Society"
}
}
};
let body_text = format!(
r#"
Hi {user}
Please use "/verify_committee code: {auth}" to verify your discord account.
Skynet Team
UL Computer Society
"#
);
// Build the message.
let email = Message::builder()
.from(sender.parse().unwrap())
.to(mail.parse().unwrap())
.subject("Skynet-Discord: Link Committee.")
.multipart(
// This is composed of two parts.
// also helps not trip spam settings (uneven number of url's
MultiPart::alternative()
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text))
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())),
)
.expect("failed to build email");
let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone());
// Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay(&config.mail_smtp)?.credentials(creds).build();
// Send the email
mailer.send(&email)
}
pub async fn get_verify_from_db(db: &Pool<Sqlite>, user: &UserId) -> Option<Committee> {
sqlx::query_as::<_, Committee>(
r#"
SELECT *
FROM committee
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
async fn save_to_db(db: &Pool<Sqlite>, email: &str, auth: &str, user: &UserId) -> Result<Option<Committee>, sqlx::Error> {
sqlx::query_as::<_, Committee>(
"
INSERT INTO committee (email, discord, auth_code)
VALUES (?1, ?2, ?3)
",
)
.bind(email.to_owned())
.bind(*user.as_u64() as i64)
.bind(auth.to_owned())
.fetch_optional(db)
.await
}
}
pub mod verify {
use super::*;
use crate::commands::committee::link::get_verify_from_db;
use serenity::model::id::{GuildId, RoleId};
use serenity::model::user::User;
use skynet_discord_bot::Committee;
use sqlx::Error;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let committee_server = GuildId(1220150752656363520);
match command.guild_id {
None => {
return "Not in correct discord server.".to_string();
}
Some(x) => {
if x != committee_server {
return "Not in correct discord server.".to_string();
}
}
}
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
// check if user has used /link_committee
let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await {
x
} else {
return "Please use /link_committee first".to_string();
};
let option = command
.data
.options
.first()
.expect("Expected code option")
.resolved
.as_ref()
.expect("Expected code object");
let code = if let CommandDataOptionValue::String(code) = option {
code
} else {
return "Please provide a verification code".to_string();
};
if &details.auth_code != code {
return "Invalid verification code".to_string();
}
match set_discord(&db, &command.user.id).await {
Ok(_) => {
// get teh right roles for the user
set_server_roles(&command.user, ctx).await;
"Discord username linked to Wolves for committee".to_string()
}
Err(e) => {
println!("{:?}", e);
"Failed to save, please try /link_committee again".to_string()
}
}
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("verify_committee")
.description("Verify Wolves Committee Email")
.create_option(|option| {
option
.name("code")
.description("Code from verification email")
.kind(CommandOptionType::String)
.required(true)
})
}
async fn set_discord(db: &Pool<Sqlite>, discord: &UserId) -> Result<Option<Committee>, Error> {
sqlx::query_as::<_, Committee>(
"
UPDATE committee
SET committee = 1
WHERE discord = ?
",
)
.bind(*discord.as_u64() as i64)
.fetch_optional(db)
.await
}
async fn set_server_roles(discord: &User, ctx: &Context) {
let committee_server = GuildId(1220150752656363520);
if let Ok(mut member) = committee_server.member(&ctx.http, &discord.id).await {
let committee_member = RoleId(1226602779968274573);
if let Err(e) = member.add_role(&ctx, committee_member).await {
println!("{:?}", e);
}
}
}
}

View file

@ -4,21 +4,17 @@ use lettre::{
Message, SmtpTransport, Transport,
};
use maud::html;
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
id::UserId,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify};
use serenity::{builder::CreateCommand, client::Context, model::id::UserId};
use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify};
use skynet_discord_bot::{get_now_iso, random_string, Config};
use sqlx::{Pool, Sqlite};
pub mod link {
use super::*;
use serde::{Deserialize, Serialize};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
@ -41,16 +37,11 @@ pub mod link {
return "Linking already in process, please check email.".to_string();
}
let option = command
.data
.options
.first()
.expect("Expected email option")
.resolved
.as_ref()
.expect("Expected email object");
let email = if let CommandDataOptionValue::String(email) = option {
let email = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(email),
..
}) = command.data.options.first()
{
email.trim()
} else {
return "Please provide a valid user".to_string();
@ -112,11 +103,10 @@ pub mod link {
format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email)
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link_wolves")
pub fn register() -> CreateCommand {
CreateCommand::new("link_wolves")
.description("Set Wolves Email")
.create_option(|option| option.name("email").description("UL Wolves Email").kind(CommandOptionType::String).required(true))
.add_option(CreateCommandOption::new(CommandOptionType::String, "email", "UL Wolves Email").required(true))
}
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
@ -127,7 +117,7 @@ pub mod link {
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.fetch_one(db)
.await
.ok()
@ -239,7 +229,7 @@ pub mod link {
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.fetch_one(db)
.await
.ok()
@ -253,13 +243,26 @@ pub mod link {
",
)
.bind(record.email.to_owned())
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.bind(auth.to_owned())
.bind(get_now_iso(false))
.fetch_optional(db)
.await
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum WolvesResultUserResult {
B(bool),
S(String),
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
success: i64,
result: WolvesResultUserResult,
}
async fn save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> {
sqlx::query_as::<_, Wolves>(
"
@ -278,11 +281,13 @@ pub mod link {
pub mod verify {
use super::*;
use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::model::user::User;
use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers};
use skynet_discord_bot::common::database::get_server_config;
use skynet_discord_bot::common::database::{ServerMembersWolves, Servers};
use sqlx::Error;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
@ -296,16 +301,11 @@ pub mod verify {
return "Please use /link_wolves first".to_string();
};
let option = command
.data
.options
.first()
.expect("Expected code option")
.resolved
.as_ref()
.expect("Expected code object");
let code = if let CommandDataOptionValue::String(code) = option {
let code = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(code),
..
}) = command.data.options.first()
{
code
} else {
return "Please provide a verification code".to_string();
@ -337,14 +337,10 @@ pub mod verify {
"Failed to verify".to_string()
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("verify").description("Verify Wolves Email").create_option(|option| {
option
.name("code")
.description("Code from verification email")
.kind(CommandOptionType::String)
.required(true)
})
pub fn register() -> CreateCommand {
CreateCommand::new("verify")
.description("Verify Wolves Email")
.add_option(CreateCommandOption::new(CommandOptionType::String, "code", "Code from verification email").required(true))
}
async fn db_pending_clear_successful(pool: &Pool<Sqlite>, user: &UserId) -> Result<Option<WolvesVerify>, Error> {
@ -355,7 +351,7 @@ pub mod verify {
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.fetch_optional(pool)
.await
}
@ -368,7 +364,7 @@ pub mod verify {
WHERE email = ?
",
)
.bind(*discord.as_u64() as i64)
.bind(discord.get() as i64)
.bind(email)
.fetch_optional(db)
.await
@ -377,7 +373,7 @@ pub mod verify {
async fn set_server_roles(db: &Pool<Sqlite>, discord: &User, ctx: &Context) {
if let Ok(servers) = get_servers(db, &discord.id).await {
for server in servers {
if let Ok(mut member) = server.server.member(&ctx.http, &discord.id).await {
if let Ok(member) = server.server.member(&ctx.http, &discord.id).await {
if let Some(config) = get_server_config(db, &server.server).await {
let Servers {
role_past,
@ -415,7 +411,7 @@ pub mod verify {
WHERE discord = ?
",
)
.bind(*discord.as_u64() as i64)
.bind(discord.get() as i64)
.fetch_all(db)
.await
}

View file

@ -1,13 +1,6 @@
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use serenity::{builder::CreateCommand, client::Context};
use skynet_discord_bot::DataBase;
use skynet_discord_bot::common::database::DataBase;
use sqlx::{Pool, Sqlite};
pub(crate) mod user {
@ -16,31 +9,21 @@ pub(crate) mod user {
use super::*;
use crate::commands::link_email::link::get_server_member_discord;
use serde::{Deserialize, Serialize};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::model::id::UserId;
use skynet_discord_bot::{whitelist_update, Config, Minecraft, Wolves};
use skynet_discord_bot::common::database::Wolves;
use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft};
use skynet_discord_bot::Config;
use sqlx::Error;
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link_minecraft")
pub fn register() -> CreateCommand {
CreateCommand::new("link_minecraft")
.description("Link your minecraft account")
.create_option(|option| {
option
.name("minecraft_username")
.description("Your Minecraft username")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("bedrock_account")
.description("Is this a Bedrock account?")
.kind(CommandOptionType::Boolean)
.required(false)
})
.add_option(CreateCommandOption::new(CommandOptionType::String, "minecraft_username", "Your Minecraft username").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "bedrock_account", "Is this a Bedrock account?").required(false))
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
@ -58,14 +41,10 @@ pub(crate) mod user {
return "Not linked with wolves, please use ``/link_wolves`` with your wolves email.".to_string();
}
let username = if let CommandDataOptionValue::String(username) = command
.data
.options
.first()
.expect("Expected username option")
.resolved
.as_ref()
.expect("Expected username object")
let username = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(username),
..
}) = command.data.options.first()
{
username.trim()
} else {
@ -73,12 +52,15 @@ pub(crate) mod user {
};
// this is always true unless they state its not
let mut java = true;
if let Some(x) = command.data.options.get(1) {
if let Some(CommandDataOptionValue::Boolean(z)) = x.to_owned().resolved {
java = !z;
}
}
let java = if let Some(CommandDataOption {
value: CommandDataOptionValue::Boolean(z),
..
}) = command.data.options.get(1)
{
!z
} else {
true
};
let username_mc;
if java {
@ -128,7 +110,7 @@ pub(crate) mod user {
WHERE discord = ?1;
",
)
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
@ -173,7 +155,7 @@ pub(crate) mod user {
WHERE discord = ?1;
",
)
.bind(*user.as_u64() as i64)
.bind(user.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
@ -192,7 +174,7 @@ pub(crate) mod user {
) sub on minecraft.server_discord = sub.server
",
)
.bind(*discord.as_u64() as i64)
.bind(discord.get() as i64)
.fetch_all(db)
.await
}
@ -203,23 +185,22 @@ pub(crate) mod server {
use super::*;
pub(crate) mod add {
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
use serenity::model::id::GuildId;
use sqlx::Error;
// this is to managfe the server side of commands related to minecraft
use super::*;
use skynet_discord_bot::{is_admin, update_server, Config, Minecraft};
use skynet_discord_bot::common::minecraft::update_server;
use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::{is_admin, Config};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_add").description("Add a minecraft server").create_option(|option| {
option
.name("server_id")
.description("ID of the Minecraft server hosted by the Computer Society")
.kind(CommandOptionType::String)
.required(true)
})
pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_add").description("Add a minecraft server").add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
)
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
@ -229,16 +210,12 @@ pub(crate) mod server {
Some(x) => x,
};
let server_minecraft = if let CommandDataOptionValue::String(id) = command
.data
.options
.first()
.expect("Expected server_id option")
.resolved
.as_ref()
.expect("Expected server_id object")
let server_minecraft = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(id),
..
}) = command.data.options.first()
{
id.to_owned()
id.to_string()
} else {
return String::from("Expected Server ID");
};
@ -275,7 +252,7 @@ pub(crate) mod server {
VALUES (?1, ?2)
",
)
.bind(*discord.as_u64() as i64)
.bind(discord.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
@ -283,16 +260,18 @@ pub(crate) mod server {
}
pub(crate) mod list {
use serenity::builder::CreateApplicationCommand;
use serenity::all::CommandInteraction;
use serenity::builder::CreateCommand;
use serenity::client::Context;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use skynet_discord_bot::{get_minecraft_config_server, is_admin, server_information, Config, DataBase};
use skynet_discord_bot::common::database::DataBase;
use skynet_discord_bot::common::minecraft::{get_minecraft_config_server, server_information};
use skynet_discord_bot::{is_admin, Config};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_list").description("List your minecraft servers")
pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_list").description("List your minecraft servers")
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
@ -342,25 +321,22 @@ pub(crate) mod server {
}
pub(crate) mod delete {
use serenity::builder::CreateApplicationCommand;
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::builder::CreateCommand;
use serenity::client::Context;
use serenity::model::application::command::CommandOptionType;
use serenity::model::id::GuildId;
use serenity::model::prelude::application_command::{ApplicationCommandInteraction, CommandDataOptionValue};
use skynet_discord_bot::{is_admin, DataBase, Minecraft};
use skynet_discord_bot::common::database::DataBase;
use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_delete").description("Delete a minecraft server").create_option(|option| {
option
.name("server_id")
.description("ID of the Minecraft server hosted by the Computer Society")
.kind(CommandOptionType::String)
.required(true)
})
pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_delete").description("Delete a minecraft server").add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
)
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
@ -370,16 +346,12 @@ pub(crate) mod server {
Some(x) => x,
};
let server_minecraft = if let CommandDataOptionValue::String(id) = command
.data
.options
.first()
.expect("Expected server_id option")
.resolved
.as_ref()
.expect("Expected server_id object")
let server_minecraft = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(id),
..
}) = command.data.options.first()
{
id.to_owned()
id.to_string()
} else {
return String::from("Expected Server ID");
};
@ -410,7 +382,7 @@ pub(crate) mod server {
WHERE server_discord = ?1 AND server_minecraft = ?2
",
)
.bind(*discord.as_u64() as i64)
.bind(discord.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await

View file

@ -1,5 +1,4 @@
pub mod add_server;
pub mod committee;
pub mod link_email;
pub mod minecraft;
pub mod role_adder;

View file

@ -1,62 +1,45 @@
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use serenity::client::Context;
use skynet_discord_bot::{is_admin, DataBase, RoleAdder};
use skynet_discord_bot::common::database::{DataBase, RoleAdder};
use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite};
pub mod edit {
use super::*;
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let role_a = if let CommandDataOptionValue::Role(role) = command
.data
.options
.first()
.expect("Expected role option")
.resolved
.as_ref()
.expect("Expected role object")
let role_a = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.first()
{
role.id.to_owned()
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let role_b = if let CommandDataOptionValue::Role(role) = command
.data
.options
.get(1)
.expect("Expected role option")
.resolved
.as_ref()
.expect("Expected role object")
let role_b = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(1)
{
role.id.to_owned()
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let role_c = if let CommandDataOptionValue::Role(role) = command
.data
.options
.get(2)
.expect("Expected role option")
.resolved
.as_ref()
.expect("Expected role object")
let role_c = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(2)
{
role.id.to_owned()
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
@ -69,14 +52,15 @@ pub mod edit {
return "Role C cannot be same as A or B".to_string();
}
let mut delete = false;
if let Some(x) = command.data.options.get(3) {
let tmp = x.to_owned();
if let Some(CommandDataOptionValue::Boolean(z)) = tmp.resolved {
delete = z;
}
}
let delete = if let Some(CommandDataOption {
value: CommandDataOptionValue::Boolean(z),
..
}) = command.data.options.get(3)
{
*z
} else {
false
};
let db_lock = {
let data_read = ctx.data.read().await;
@ -123,32 +107,13 @@ pub mod edit {
}
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("roles_adder")
pub fn register() -> CreateCommand {
CreateCommand::new("roles_adder")
.description("Combine roles together to an new one")
.create_option(|option| {
option
.name("role_a")
.description("A role you want to add to Role B")
.kind(CommandOptionType::Role)
.required(true)
})
.create_option(|option| {
option
.name("role_b")
.description("A role you want to add to Role A")
.kind(CommandOptionType::Role)
.required(true)
})
.create_option(|option| option.name("role_c").description("Sum of A and B").kind(CommandOptionType::Role).required(true))
.create_option(|option| {
option
.name("delete")
.description("Delete this entry.")
.kind(CommandOptionType::Boolean)
.required(false)
})
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_a", "A role you want to add to Role B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_b", "A role you want to add to Role A").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_c", "Sum of A and B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "delete", "Delete this entry.").required(false))
}
async fn add_server(db: &Pool<Sqlite>, server: &RoleAdder, delete: bool) -> Result<Option<RoleAdder>, Error> {
@ -159,10 +124,10 @@ pub mod edit {
WHERE server = ?1 AND role_a = ?2 AND role_b = ?3 AND role_c = ?4
",
)
.bind(*server.server.as_u64() as i64)
.bind(*server.role_a.as_u64() as i64)
.bind(*server.role_b.as_u64() as i64)
.bind(*server.role_c.as_u64() as i64)
.bind(server.server.get() as i64)
.bind(server.role_a.get() as i64)
.bind(server.role_b.get() as i64)
.bind(server.role_c.get() as i64)
.fetch_optional(db)
.await
} else {
@ -172,10 +137,10 @@ pub mod edit {
VALUES (?1, ?2, ?3, ?4)
",
)
.bind(*server.server.as_u64() as i64)
.bind(*server.role_a.as_u64() as i64)
.bind(*server.role_b.as_u64() as i64)
.bind(*server.role_c.as_u64() as i64)
.bind(server.server.get() as i64)
.bind(server.role_a.get() as i64)
.bind(server.role_b.get() as i64)
.bind(server.role_c.get() as i64)
.fetch_optional(db)
.await
}
@ -188,10 +153,10 @@ pub mod list {}
pub mod tools {
use serenity::client::Context;
use serenity::model::guild::Member;
use skynet_discord_bot::RoleAdder;
use skynet_discord_bot::common::database::RoleAdder;
use sqlx::{Pool, Sqlite};
pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, mut new_data: Member) {
pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, new_data: Member) {
// check if the role changed is part of the oens for this server
if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>(
r#"
@ -200,7 +165,7 @@ pub mod tools {
WHERE server = ?
"#,
)
.bind(*new_data.guild_id.as_u64() as i64)
.bind(new_data.guild_id.get() as i64)
.fetch_all(db)
.await
{

260
src/common/database.rs Normal file
View file

@ -0,0 +1,260 @@
use crate::Config;
use serde::{Deserialize, Serialize};
use serenity::model::guild;
use serenity::model::id::{ChannelId, GuildId, RoleId, UserId};
use serenity::prelude::TypeMapKey;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow};
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct DataBase;
impl TypeMapKey for DataBase {
type Value = Arc<RwLock<Pool<Sqlite>>>;
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
pub minecraft_uid: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
email: row.try_get("email")?,
discord: get_discord_from_row(row),
minecraft: row.try_get("minecraft")?,
minecraft_uid: row.try_get("minecraft_uid")?,
})
}
}
fn get_discord_from_row(row: &SqliteRow) -> Option<UserId> {
match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
pub id_wolves: i64,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord: get_discord_from_row(row),
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
date_expiry: row.try_get("date_expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
pub server: GuildId,
pub wolves_api: String,
pub role_past: Option<RoleId>,
pub role_current: RoleId,
pub member_past: i64,
pub member_current: i64,
pub bot_channel_id: ChannelId,
pub server_name: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let role_past = match row.try_get("role_past") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
let role_current = match row.try_get("role_current") {
Ok(x) => {
let tmp: i64 = x;
RoleId::from(tmp as u64)
}
_ => RoleId::from(0u64),
};
let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
Ok(Self {
server,
wolves_api: row.try_get("wolves_api")?,
role_past,
role_current,
member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?,
bot_channel_id,
server_name: row.try_get("server_name")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
pub server: GuildId,
pub role_a: RoleId,
pub role_b: RoleId,
pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
role_a: get_role_from_row(row, "role_a"),
role_b: get_role_from_row(row, "role_b"),
role_c: get_role_from_row(row, "role_c"),
})
}
}
fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
RoleId::new(tmp as u64)
}
_ => RoleId::from(0u64),
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await?;
Ok(pool)
}
pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
WHERE server = ?
"#,
)
.bind(server.get() as i64)
.fetch_one(db)
.await
.ok()
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE server = ? AND discord = ?
"#,
)
.bind(server.get() as i64)
.bind(member.user.id.get() as i64)
.fetch_one(db)
.await
}
pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}

174
src/common/minecraft.rs Normal file
View file

@ -0,0 +1,174 @@
use crate::common::set_roles::normal::get_server_member_bulk;
use crate::Config;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serenity::model::id::GuildId;
use sqlx::sqlite::SqliteRow;
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft {
pub discord: GuildId,
pub minecraft: String,
}
impl<'r> FromRow<'r, SqliteRow> for Minecraft {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server_discord")?;
let discord = GuildId::from(server_tmp as u64);
Ok(Self {
discord,
minecraft: row.try_get("server_minecraft")?,
})
}
}
/**
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<Sqlite>, 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, true));
}
if let Some(x) = member.minecraft_uid {
usernames.push((x, false));
}
}
if !usernames.is_empty() {
whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
}
}
pub async fn post<T: Serialize>(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<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
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<String>,
}
pub async fn whitelist_wipe(server: &str, token: &str) {
let url_base = format!("https://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::<Vec<&str>>(&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<ServerDetailsRes> {
let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
get::<ServerDetailsRes>(&format!("{url_base}/"), &bearer).await
}
pub async fn get_minecraft_config(db: &Pool<Sqlite>) -> Vec<Minecraft> {
sqlx::query_as::<_, Minecraft>(
r#"
SELECT *
FROM minecraft
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Vec<Minecraft> {
sqlx::query_as::<_, Minecraft>(
r#"
SELECT *
FROM minecraft
WHERE server_discord = ?1
"#,
)
.bind(g_id.get() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) {
println!("Update whitelist for {}", server);
let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
for (name, java) in add {
let data = if *java {
BodyCommand {
command: format!("whitelist add {name}"),
}
} else {
BodyCommand {
command: format!("fwhitelist add {name}"),
}
};
post(&format!("{url_base}/command"), &bearer, &data).await;
}
}

4
src/common/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod database;
pub mod minecraft;
pub mod set_roles;
pub mod wolves;

393
src/common/set_roles.rs Normal file
View file

@ -0,0 +1,393 @@
pub mod normal {
use crate::common::database::{DataBase, ServerMembersWolves, Servers, Wolves};
use crate::get_now_iso;
use serenity::client::Context;
use serenity::model::id::{GuildId, RoleId, UserId};
use sqlx::{Pool, Sqlite};
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().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 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.get(), roles_set[0], roles_set[1], roles_set[2]);
}
pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
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.get() as i64)
.bind(get_now_iso(true))
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn set_server_numbers(db: &Pool<Sqlite>, 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.get() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into {}", server.get());
println!("{:?}", e);
}
}
}
}
// for updating committee members
pub mod committee {
use crate::common::database::{DataBase, Wolves};
use crate::common::wolves::committees::Committees;
use crate::Config;
use serenity::all::EditRole;
use serenity::builder::CreateChannel;
use serenity::client::Context;
use serenity::model::channel::ChannelType;
use serenity::model::guild::Member;
use serenity::model::id::ChannelId;
use serenity::model::prelude::RoleId;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
use std::sync::Arc;
pub async fn check_committee(ctx: Arc<Context>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config_global = config_lock.read().await;
let server = config_global.committee_server;
// because to use it to update a single user we need to pre-get the members of teh server
let mut members = server.members(&ctx, None, None).await.unwrap_or_default();
update_committees(&db, &ctx, &config_global, &mut members).await;
}
/**
This function can take a vec of members (or just one) and gives tehm the appropiate roles on teh committee server
*/
pub async fn update_committees(db: &Pool<Sqlite>, ctx: &Context, config: &Config, members: &mut Vec<Member>) {
let server = config.committee_server;
let committee_member = RoleId::new(1226602779968274573);
let committees = get_committees(db).await;
let categories = vec![
// C&S Chats 1
ChannelId::new(1226606560973815839),
// C&S Chats 2
ChannelId::new(1341457244973305927),
// C&S Chats 3
ChannelId::new(1341457509717639279),
];
// information about the server
let roles = server.roles(&ctx).await.unwrap_or_default();
let channels = server.channels(&ctx).await.unwrap_or_default();
// make a hashmap of the nameof roles to quickly get them out again
let mut roles_name = HashMap::new();
for role in roles.values() {
roles_name.insert(role.name.to_owned(), role.to_owned());
}
let mut channels_name = HashMap::new();
for channel in channels.values() {
// we only care about teh channels in teh category
if let Some(x) = channel.parent_id {
for category in &categories {
if x.eq(category) {
channels_name.insert(channel.name.to_owned(), channel.to_owned());
}
}
}
}
// a map of users and the roles they are goign to be getting
let mut users_roles = HashMap::new();
// a list of all the roles that can be removed from folks who should have them
let mut committee_roles = vec![committee_member];
let mut category_index = 0;
let mut i = 0;
loop {
if i >= committees.len() {
break;
}
let committee = &committees[i];
// get the role for this committee/club/soc
let role = match roles_name.get(&committee.name_full) {
Some(x) => Some(x.to_owned()),
None => {
// create teh role if it does not exist
match server
.create_role(&ctx, EditRole::new().name(&committee.name_full).hoist(false).mentionable(true))
.await
{
Ok(x) => Some(x),
Err(_) => None,
}
}
};
// create teh channel if it does nto exist
if !channels_name.contains_key(&committee.name_profile) {
match server
.create_channel(
&ctx,
CreateChannel::new(&committee.name_profile)
.kind(ChannelType::Text)
.category(categories[category_index]),
)
.await
{
Ok(x) => {
// update teh channels name list
channels_name.insert(x.name.to_owned(), x.to_owned());
println!("Created channel: {}", &committee.name_profile);
}
Err(x) => {
let tmp = x.to_string();
if x.to_string().contains("Maximum number of channels in category reached (50)") {
category_index += 1;
continue;
}
dbg!("Unable to create channel: ", &tmp, &tmp.contains("Maximum number of channels in category reached (50)"));
}
}
};
// so if the role exists
if let Some(r) = role {
committee_roles.push(r.id);
for id_wolves in &committee.committee {
// ID in this is the wolves ID, so we need to get a matching discord ID (if one exists)
if let Some(x) = get_server_member_discord(db, id_wolves).await {
if let Some(member_tmp) = x.discord {
let values = users_roles.entry(member_tmp).or_insert(vec![committee_member]);
values.push(r.id);
}
}
}
}
i += 1;
}
// now we have a map of all users that should get roles time to go through all the folks on teh server
for member in members {
let roles_current = member.roles(ctx).unwrap_or_default();
let roles_required = match users_roles.get(&member.user.id) {
None => {
vec![]
}
Some(x) => {
let mut tmp = x.to_owned();
if !tmp.is_empty() {
tmp.push(committee_member);
}
tmp
}
};
let mut roles_rem = vec![];
let mut roles_add = vec![];
// get a list of all the roles to remove from someone
let mut roles_current_id = vec![];
for role in &roles_current {
roles_current_id.push(role.id.to_owned());
if !roles_required.contains(&role.id) {
roles_rem.push(role.id.to_owned());
}
}
if !roles_required.is_empty() {
// if there are committee roles then give the general purporse role
roles_add.push(committee_member);
}
for role in &roles_required {
if !roles_current_id.contains(role) {
roles_add.push(role.to_owned());
}
}
if !roles_rem.is_empty() {
member.remove_roles(&ctx, &roles_rem).await.unwrap_or_default();
}
if !roles_add.is_empty() {
// these roles are flavor roles, only there to make folks mentionable
member.add_roles(&ctx, &roles_add).await.unwrap_or_default();
} else {
member.remove_roles(&ctx, &[committee_member]).await.unwrap_or_default();
}
}
// finally re-order teh channels to make them visually apealing
let mut channel_names = channels_name.clone().into_keys().collect::<Vec<String>>();
channel_names.sort();
// get a list of all teh new positions
let mut new_positions = vec![];
for (i, name) in channel_names.iter().enumerate() {
if let Some(channel) = channels_name.get_mut(name) {
let position_new = i as u64;
if position_new != channel.position as u64 {
new_positions.push((channel.id.to_owned(), position_new));
}
}
}
if !new_positions.is_empty() {
match server.reorder_channels(&ctx, new_positions).await {
Ok(_) => {
println!("Successfully re-orderd the committee category");
}
Err(e) => {
dbg!("Failed to re-order ", e);
}
}
}
}
async fn get_committees(db: &Pool<Sqlite>) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
FROM committees
"#,
)
.fetch_all(db)
.await
.unwrap_or_else(|e| {
dbg!(e);
vec![]
})
}
async fn get_server_member_discord(db: &Pool<Sqlite>, user: &i64) -> Option<Wolves> {
sqlx::query_as::<_, Wolves>(
r#"
SELECT *
FROM wolves
WHERE id_wolves = ?
"#,
)
.bind(user)
.fetch_one(db)
.await
.ok()
}
}

278
src/common/wolves.rs Normal file
View file

@ -0,0 +1,278 @@
use crate::common::database::Wolves;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
/**
This file relates to anything that directly interacts with teh wolves API
*/
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUserMin {
// committee: String,
member_id: String,
// first_name: String,
// last_name: String,
contact_email: String,
// opt_in_email: String,
// student_id: Option<String>,
// note: Option<String>,
// expiry: String,
// requested: String,
// approved: String,
// sitename: String,
// domain: String,
}
async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesResultUserMin) {
// expiry
match sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {:?}", user);
println!("{:?}", e);
}
}
}
/**
This is getting data for Clubs and Socs
*/
pub mod cns {
use crate::common::database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers};
use crate::common::set_roles::normal::update_server;
use crate::common::wolves::{add_users_wolves, WolvesResultUserMin};
use crate::Config;
use serenity::client::Context;
use serenity::model::id::GuildId;
use sqlx::{Pool, Sqlite};
use std::collections::BTreeMap;
impl From<&wolves_oxidised::WolvesUser> for WolvesResultUserMin {
fn from(value: &wolves_oxidised::WolvesUser) -> Self {
Self {
member_id: value.member_id.to_owned(),
contact_email: value.contact_email.to_owned(),
}
}
}
pub async fn get_wolves(ctx: &Context) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
// set up teh client
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
// this is the unique api key for each club/soc
wolves_api,
server_name,
..
} = &server_config;
// dbg!(&server_config);
let existing_tmp = get_server_member(&db, server).await;
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// list of users that need to be updated for this server
let mut user_to_update = vec![];
let mut server_name_tmp = None;
for user in wolves.get_members(wolves_api).await {
// dbg!(&user.committee);
if server_name_tmp.is_none() {
server_name_tmp = Some(user.committee.to_owned());
}
let id = user.member_id.parse::<u64>().unwrap_or_default();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
add_users_server_members(&db, server, &user).await;
}
Some(old) => {
// always update wolves table, in case data has changed
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
}
}
}
}
if let Some(name) = server_name_tmp {
if &name != server_name {
set_server_member(&db, server, &name).await;
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
async fn set_server_member(db: &Pool<Sqlite>, server: &GuildId, name: &str) {
match sqlx::query_as::<_, Servers>(
"
UPDATE servers
SET server_name = ?
WHERE server = ?
",
)
.bind(name)
.bind(server.get() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to set server name {}", server.get());
println!("{:?}", e);
}
}
}
async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
)
"#,
)
.bind(server.get() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &wolves_oxidised::WolvesUser) {
match sqlx::query_as::<_, ServerMembers>(
"
INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
VALUES (?1, ?2, ?3)
",
)
.bind(server.get() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.get(), user);
println!("{:?}", e);
}
}
}
}
/**
Get and store the data on C&S committees
*/
pub mod committees {
use crate::common::database::DataBase;
use crate::Config;
use serenity::client::Context;
use sqlx::{Pool, Sqlite};
// Database entry for it
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Committees {
pub id: i64,
pub name_full: String,
pub name_profile: String,
pub name_plain: String,
pub link: String,
#[sqlx(json)]
pub committee: Vec<i64>,
}
impl From<wolves_oxidised::WolvesCNS> for Committees {
fn from(value: wolves_oxidised::WolvesCNS) -> Self {
Self {
id: value.id,
name_full: value.name_full,
name_profile: value.name_profile,
name_plain: value.name_plain,
link: value.link,
committee: value.committee,
}
}
}
pub async fn get_cns(ctx: &Context) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
// request data from wolves
for committee_wolves in wolves.get_committees().await {
let committee = Committees::from(committee_wolves);
add_committee(&db, &committee).await;
}
}
async fn add_committee(db: &Pool<Sqlite>, committee: &Committees) {
match sqlx::query_as::<_, Committees>(
"
INSERT INTO committees (id, name_profile, name_full, name_plain, link, committee)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT(id) DO UPDATE SET committee = $6
",
)
.bind(committee.id)
.bind(&committee.name_profile)
.bind(&committee.name_full)
.bind(&committee.name_plain)
.bind(&committee.link)
.bind(serde_json::to_string(&committee.committee).unwrap_or_default())
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Committees {:?}", committee);
println!("{:?}", e);
}
}
}
}

View file

@ -1,25 +1,13 @@
use dotenvy::dotenv;
use serde::{Deserialize, Serialize};
use serenity::{
model::{
guild,
id::{ChannelId, GuildId, RoleId},
},
prelude::TypeMapKey,
};
pub mod common;
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 dotenvy::dotenv;
use rand::{distr::Alphanumeric, rng, Rng};
use serenity::all::CommandInteraction;
use serenity::client::Context;
use serenity::model::id::UserId;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},
Error, FromRow, Pool, Row, Sqlite,
};
use std::{env, str::FromStr, sync::Arc};
use serenity::model::id::{ChannelId, GuildId, RoleId};
use serenity::prelude::TypeMapKey;
use std::{env, sync::Arc};
use tokio::sync::RwLock;
#[derive(Debug)]
@ -42,16 +30,16 @@ pub struct Config {
pub wolves_url: String,
// API key for accessing more general resources
pub wolves_api: String,
// discord server for committee
pub committee_server: GuildId,
pub committee_role: RoleId,
pub committee_category: ChannelId,
}
impl TypeMapKey for Config {
type Value = Arc<RwLock<Config>>;
}
pub struct DataBase;
impl TypeMapKey for DataBase {
type Value = Arc<RwLock<Pool<Sqlite>>>;
}
pub fn get_config() -> Config {
dotenv().ok();
@ -69,6 +57,9 @@ pub fn get_config() -> Config {
mail_pass: "".to_string(),
wolves_url: "".to_string(),
wolves_api: "".to_string(),
committee_server: GuildId::new(1),
committee_role: RoleId::new(1),
committee_category: ChannelId::new(1),
};
if let Ok(x) = env::var("DATABASE_HOME") {
@ -105,296 +96,23 @@ pub fn get_config() -> Config {
config.wolves_api = x.trim().to_string();
}
config
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
pub minecraft_uid: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
minecraft_uid: row.try_get("minecraft_uid")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
pub id_wolves: i64,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
date_expiry: row.try_get("date_expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Committee {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub committee: i64,
}
impl<'r> FromRow<'r, SqliteRow> for Committee {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
committee: row.try_get("committee")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
pub server: GuildId,
pub wolves_api: String,
pub role_past: Option<RoleId>,
pub role_current: RoleId,
pub member_past: i64,
pub member_current: i64,
pub bot_channel_id: ChannelId,
// these can be removed in teh future with an API update
pub server_name: String,
pub wolves_link: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let role_past = match row.try_get("role_past") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
let role_current = match row.try_get("role_current") {
Ok(x) => {
let tmp: i64 = x;
RoleId::from(tmp as u64)
}
_ => RoleId::from(0u64),
};
let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
Ok(Self {
server,
wolves_api: row.try_get("wolves_api")?,
role_past,
role_current,
member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?,
bot_channel_id,
server_name: row.try_get("server_name")?,
wolves_link: row.try_get("wolves_link")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft {
pub discord: GuildId,
pub minecraft: String,
}
impl<'r> FromRow<'r, SqliteRow> for Minecraft {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server_discord")?;
let discord = GuildId::from(server_tmp as u64);
Ok(Self {
discord,
minecraft: row.try_get("server_minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
pub server: GuildId,
pub role_a: RoleId,
pub role_b: RoleId,
pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
role_a: get_role_from_row(row, "role_a"),
role_b: get_role_from_row(row, "role_b"),
role_c: get_role_from_row(row, "role_c"),
})
}
}
fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
RoleId(tmp as u64)
if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_server = GuildId::new(x);
}
}
if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_role = RoleId::new(x);
}
}
if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_category = ChannelId::new(x);
}
_ => RoleId::from(0u64),
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await?;
Ok(pool)
}
pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
WHERE server = ?
"#,
)
.bind(*server.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE server = ? AND discord = ?
"#,
)
.bind(*server.as_u64() as i64)
.bind(*member.user.id.as_u64() as i64)
.fetch_one(db)
.await
}
pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
config
}
pub fn get_now_iso(short: bool) -> String {
@ -407,262 +125,13 @@ pub fn get_now_iso(short: bool) -> String {
}
pub fn random_string(len: usize) -> String {
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
pub mod set_roles {
use super::*;
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().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<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
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<Sqlite>, 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);
}
}
}
}
pub mod get_data {
use super::*;
use crate::set_roles::update_server;
use std::collections::BTreeMap;
use wolves_oxidised::WolvesUser;
pub async fn get_wolves(ctx: &Context) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
// set up teh client
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
wolves_api,
..
} = &server_config;
let existing_tmp = get_server_member(&db, server).await;
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// list of users that need to be updated for this server
let mut user_to_update = vec![];
for user in wolves.get_members(wolves_api).await {
let id = user.member_id.parse::<u64>().unwrap_or_default();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
add_users_wolves(&db, &user).await;
add_users_server_members(&db, server, &user).await;
}
Some(old) => {
// always update wolves table, in case data has changed
add_users_wolves(&db, &user).await;
if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
}
}
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
)
"#,
)
.bind(*server.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesUser) {
// expiry
match sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {:?}", user);
println!("{:?}", e);
}
}
}
async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesUser) {
match sqlx::query_as::<_, ServerMembers>(
"
INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
VALUES (?1, ?2, ?3)
",
)
.bind(*server.as_u64() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user);
println!("{:?}", e);
}
}
}
rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
/**
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<String> {
pub async fn is_admin(command: &CommandInteraction, ctx: &Context) -> Option<String> {
let mut admin = false;
let g_id = match command.guild_id {
@ -696,153 +165,3 @@ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) ->
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<Sqlite>, 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, true));
}
if let Some(x) = member.minecraft_uid {
usernames.push((x, false));
}
}
if !usernames.is_empty() {
whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
}
}
async fn post<T: Serialize>(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<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
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<String>,
}
pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) {
println!("Update whitelist for {}", server);
let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
for (name, java) in add {
let data = if *java {
BodyCommand {
command: format!("whitelist add {name}"),
}
} else {
BodyCommand {
command: format!("fwhitelist add {name}"),
}
};
post(&format!("{url_base}/command"), &bearer, &data).await;
}
}
pub async fn whitelist_wipe(server: &str, token: &str) {
println!("Wiping whitelist for {}", server);
let url_base = format!("https://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::<Vec<&str>>(&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<ServerDetailsRes> {
println!("Get server information for {}", server);
let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
get::<ServerDetailsRes>(&format!("{url_base}/"), &bearer).await
}
pub async fn get_minecraft_config(db: &Pool<Sqlite>) -> Vec<Minecraft> {
sqlx::query_as::<_, Minecraft>(
r#"
SELECT *
FROM minecraft
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Vec<Minecraft> {
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()
}

View file

@ -1,19 +1,22 @@
pub mod commands;
use crate::commands::role_adder::tools::on_role_change;
use serenity::all::{ActivityData, Command, CreateMessage, EditInteractionResponse, GuildMemberUpdateEvent, Interaction};
use serenity::model::guild::Member;
use serenity::{
async_trait,
client::{Context, EventHandler},
model::{
application::{command::Command, interaction::Interaction},
gateway::{GatewayIntents, Ready},
prelude::Activity,
user::OnlineStatus,
},
Client,
};
use skynet_discord_bot::{db_init, get_config, get_server_config, get_server_member, Config, DataBase};
use skynet_discord_bot::common::database::{db_init, get_server_config, get_server_member, DataBase};
use skynet_discord_bot::common::set_roles::committee::update_committees;
use skynet_discord_bot::common::wolves::committees::Committees;
use skynet_discord_bot::{get_config, Config};
use sqlx::{Pool, Sqlite};
use std::sync::Arc;
use tokio::sync::RwLock;
@ -22,14 +25,28 @@ struct Handler;
#[async_trait]
impl EventHandler for Handler {
// handles previously linked accounts joining the server
async fn guild_member_addition(&self, ctx: Context, mut new_member: Member) {
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let db = db_lock.read().await;
let config = match get_server_config(&db, &new_member.guild_id).await {
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config_global = config_lock.read().await;
// committee server takes priority
if new_member.guild_id.eq(&config_global.committee_server) {
let mut member = vec![new_member.clone()];
update_committees(&db, &ctx, &config_global, &mut member).await;
return;
}
let config_server = match get_server_config(&db, &new_member.guild_id).await {
None => return,
Some(x) => x,
};
@ -37,56 +54,75 @@ impl EventHandler for Handler {
if get_server_member(&db, &new_member.guild_id, &new_member).await.is_ok() {
let mut roles = vec![];
if let Some(role) = &config.role_past {
if let Some(role) = &config_server.role_past {
if !new_member.roles.contains(role) {
roles.push(role.to_owned());
}
}
if !new_member.roles.contains(&config.role_current) {
roles.push(config.role_current.to_owned());
if !new_member.roles.contains(&config_server.role_current) {
roles.push(config_server.role_current.to_owned());
}
if let Err(e) = new_member.add_roles(&ctx, &roles).await {
println!("{:?}", e);
}
} else {
let msg = format!(
r#"
let tmp = get_committee(&db, &config_server.server_name).await;
if !tmp.is_empty() {
let committee = &tmp[0];
let msg = format!(
r#"
Welcome {} to the {} server!
Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/link_wolves`` to get full access.
"#,
new_member.display_name(),
&config.server_name,
&config.wolves_link,
&config.server,
&config.bot_channel_id
);
new_member.display_name(),
committee.name_full,
committee.link,
&config_server.server,
&config_server.bot_channel_id
);
if let Err(err) = new_member.user.direct_message(&ctx, |m| m.content(&msg)).await {
dbg!(err);
if let Err(err) = new_member.user.direct_message(&ctx, CreateMessage::new().content(&msg)).await {
dbg!(err);
}
}
}
}
// handles role updates
async fn guild_member_update(&self, ctx: Context, _old_data: Option<Member>, new_data: Option<Member>, _: GuildMemberUpdateEvent) {
// get config/db
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let db = db_lock.read().await;
// check if the role changed is part of the oens for this server
if let Some(x) = new_data {
on_role_change(&db, &ctx, x).await;
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("[Main] {} is connected!", ready.user.name);
ctx.set_presence(Some(Activity::playing("with humanity's fate")), OnlineStatus::Online).await;
ctx.set_presence(Some(ActivityData::playing("with humanity's fate")), OnlineStatus::Online);
match Command::set_global_application_commands(&ctx.http, |commands| {
commands
.create_application_command(|command| commands::add_server::register(command))
.create_application_command(|command| commands::role_adder::edit::register(command))
.create_application_command(|command| commands::link_email::link::register(command))
.create_application_command(|command| commands::link_email::verify::register(command))
.create_application_command(|command| commands::minecraft::server::add::register(command))
.create_application_command(|command| commands::minecraft::server::list::register(command))
.create_application_command(|command| commands::minecraft::server::delete::register(command))
.create_application_command(|command| commands::minecraft::user::add::register(command))
// for committee server, temp
.create_application_command(|command| commands::committee::link::register(command))
.create_application_command(|command| commands::committee::verify::register(command))
})
match Command::set_global_commands(
&ctx.http,
vec![
commands::add_server::register(),
commands::role_adder::edit::register(),
commands::link_email::link::register(),
commands::link_email::verify::register(),
commands::minecraft::server::add::register(),
commands::minecraft::server::list::register(),
commands::minecraft::server::delete::register(),
commands::minecraft::user::add::register(),
],
)
.await
{
Ok(_) => {}
@ -97,7 +133,7 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::ApplicationCommand(command) = interaction {
if let Interaction::Command(command) = interaction {
let _ = command.defer_ephemeral(&ctx.http).await;
//println!("Received command interaction: {:#?}", command);
@ -112,31 +148,28 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
"minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await,
"minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await,
"minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await,
// for teh committee server, temporary
"link_committee" => commands::committee::link::run(&command, &ctx).await,
"verify_committee" => commands::committee::verify::run(&command, &ctx).await,
_ => "not implemented :(".to_string(),
};
if let Err(why) = command.edit_original_interaction_response(&ctx.http, |response| response.content(content)).await {
if let Err(why) = command.edit_response(&ctx.http, EditInteractionResponse::new().content(content)).await {
println!("Cannot respond to slash command: {}", why);
}
}
}
}
// handles role updates
async fn guild_member_update(&self, ctx: Context, _old_data: Option<Member>, new_data: Member) {
// get config/db
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let db = db_lock.read().await;
// check if the role changed is part of the oens for this server
on_role_change(&db, &ctx, new_data).await;
}
async fn get_committee(db: &Pool<Sqlite>, committee: &str) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
FROM committees
WHERE name_plain = ?
"#,
)
.bind(committee)
.fetch_all(db)
.await
.unwrap_or_default()
}
#[tokio::main]