2025-03-06 20:43:36 +00:00
use lettre ::{
message ::{ header , MultiPart , SinglePart } ,
transport ::smtp ::{ self , authentication ::Credentials } ,
Message , SmtpTransport , Transport ,
} ;
use maud ::html ;
use serenity ::all ::CommandOptionType ;
use serenity ::builder ::CreateCommandOption ;
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 serenity ::all ::{ CommandDataOption , CommandDataOptionValue , CommandInteraction } ;
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 ( )
} ;
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 ( ) ;
}
db_pending_clear_expired ( & db ) . await ;
if get_verify_from_db ( & db , & command . user . id ) . await . is_some ( ) {
return " Linking already in process, please check email. " . to_string ( ) ;
}
let sub_options = if let Some ( CommandDataOption {
value : CommandDataOptionValue ::SubCommand ( options ) ,
..
} ) = command . data . options . first ( )
{
options
} else {
return " Please provide sub options " . to_string ( ) ;
} ;
let email = if let Some ( x ) = sub_options . first ( ) {
match & x . value {
CommandDataOptionValue ::String ( email ) = > email . trim ( ) ,
_ = > return " Please provide a valid email " . to_string ( ) ,
}
} else {
return " Please provide a valid email " . to_string ( ) ;
} ;
// check if email exists
let details = match get_server_member_email ( & db , email ) . await {
None = > {
let invalid_user = " Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up. " . to_string ( ) ;
let wolves = wolves_oxidised ::Client ::new ( & config . wolves_url , Some ( & config . wolves_api ) ) ;
// see if the user actually exists
let id = match wolves . get_member ( email ) . await {
None = > {
return invalid_user ;
}
Some ( x ) = > x ,
} ;
// save teh user id and email to teh db
match save_to_db_user ( & db , id , email ) . await {
Ok ( x ) = > x ,
Err ( x ) = > {
dbg! ( x ) ;
return " Error: unable to save user to teh database, contact Computer Society " . to_string ( ) ;
}
} ;
// pull it back out (technically could do it in previous step but more explicit)
match get_server_member_email ( & db , email ) . await {
None = > {
return " Error: failed to read user from database. " . to_string ( ) ;
}
Some ( x ) = > x ,
}
}
Some ( x ) = > x ,
} ;
if details . discord . is_some ( ) {
return " Email already verified " . to_string ( ) ;
}
// generate a auth key
let auth = random_string ( 20 ) ;
match send_mail ( & config , & details . email , & auth , & command . user . name ) {
Ok ( _ ) = > match save_to_db ( & db , & details , & auth , & command . user . id ) . await {
Ok ( _ ) = > { }
Err ( e ) = > {
return format! ( " Unable to save to db {} {e:?} " , & details . email ) ;
}
} ,
Err ( e ) = > {
return format! ( " Unable to send mail to {} {e:?} " , & details . 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 async fn get_server_member_discord ( db : & Pool < Sqlite > , user : & UserId ) -> Option < Wolves > {
sqlx ::query_as ::< _ , Wolves > (
r #"
SELECT *
FROM wolves
WHERE discord = ?
" #,
)
. bind ( user . get ( ) as i64 )
. fetch_one ( db )
. await
. ok ( )
}
async fn get_server_member_email ( db : & Pool < Sqlite > , email : & str ) -> Option < Wolves > {
sqlx ::query_as ::< _ , Wolves > (
r #"
SELECT *
FROM wolves
WHERE email = ?
" #,
)
. bind ( email )
. fetch_one ( db )
. await
. ok ( )
}
fn send_mail ( config : & Config , mail : & str , auth : & str , user : & str ) -> Result < smtp ::response ::Response , smtp ::Error > {
let discord = " https://computer.discord.skynet.ie " ;
let sender = format! ( " UL Computer Society < {} > " , & config . mail_user ) ;
// Create the html we want to send.
let html = html! {
head {
title { " UL Wolves Discord Linker " }
style type = " text/css " {
" h2, h4 { font-family: Arial, Helvetica, sans-serif; } "
}
}
div {
h2 { " UL Wolves Discord Linker " }
h3 { " Link your UL Wolves Account to Discord " }
// Substitute in the name of our recipient.
p { " Hi " ( user ) " , " }
p {
" Please paste this line into Discord (and press enter) to verify your discord account: "
br ;
pre { " /wolves verify code: " ( auth ) }
}
hr ;
h3 { " Help & Support " }
p {
" If you have issues please refer to our Computer Society Discord Server: "
br ;
a href = ( discord ) { ( discord ) }
br ;
" UL Computer Society "
}
}
} ;
let body_text = format! (
r #"
UL Wolves Discord Linker
Link your UL Wolves Account to Discord
Link your Account
Hi { user } ,
Please paste this line into Discord ( and press enter ) to verify your Discord account :
/ wolves verify code : { auth }
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Help & Support
If you have issues please refer to our Computer Society Discord Server :
{ discord }
UL Computer Society
" #
) ;
// Build the message.
let email = Message ::builder ( )
. from ( sender . parse ( ) . unwrap ( ) )
. to ( mail . parse ( ) . unwrap ( ) )
. subject ( " Skynet: Link Discord to Wolves. " )
. 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 db_pending_clear_expired ( pool : & Pool < Sqlite > ) -> Option < WolvesVerify > {
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
DELETE
FROM wolves_verify
WHERE date_expiry < ?
" #,
)
. bind ( get_now_iso ( true ) )
. fetch_one ( pool )
. await
. ok ( )
}
pub async fn get_verify_from_db ( db : & Pool < Sqlite > , user : & UserId ) -> Option < WolvesVerify > {
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
SELECT *
FROM wolves_verify
WHERE discord = ?
" #,
)
. bind ( user . get ( ) as i64 )
. fetch_one ( db )
. await
. ok ( )
}
async fn save_to_db ( db : & Pool < Sqlite > , record : & Wolves , auth : & str , user : & UserId ) -> Result < Option < WolvesVerify > , sqlx ::Error > {
sqlx ::query_as ::< _ , WolvesVerify > (
"
INSERT INTO wolves_verify ( email , discord , auth_code , date_expiry )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 )
" ,
)
. bind ( record . email . to_owned ( ) )
. bind ( user . get ( ) as i64 )
. bind ( auth . to_owned ( ) )
. bind ( get_now_iso ( false ) )
. fetch_optional ( db )
. await
}
async fn save_to_db_user ( db : & Pool < Sqlite > , id_wolves : i64 , email : & str ) -> Result < Option < Wolves > , sqlx ::Error > {
sqlx ::query_as ::< _ , Wolves > (
"
INSERT INTO wolves ( id_wolves , email )
VALUES ( $ 1 , $ 2 )
ON CONFLICT ( id_wolves ) DO UPDATE SET email = $ 2
" ,
)
. bind ( id_wolves )
. bind ( email )
. fetch_optional ( db )
. await
}
}
pub mod verify {
use super ::* ;
use crate ::commands ::wolves ::link ::{ db_pending_clear_expired , get_server_member_discord , get_verify_from_db } ;
2025-06-16 21:50:26 +01:00
use serenity ::all ::{ CommandDataOption , CommandDataOptionValue , CommandInteraction } ;
2025-03-06 20:43:36 +00:00
use serenity ::model ::user ::User ;
use skynet_discord_bot ::common ::database ::get_server_config ;
use skynet_discord_bot ::common ::database ::{ ServerMembersWolves , Servers } ;
use skynet_discord_bot ::common ::wolves ::committees ::Committees ;
use sqlx ::Error ;
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 Database in TypeMap. " ) . clone ( )
} ;
let db = db_lock . read ( ) . await ;
// check if user has used /link_wolves
let details = if let Some ( x ) = get_verify_from_db ( & db , & command . user . id ) . await {
x
} else {
return " Please use ''/wolves link'' first " . to_string ( ) ;
} ;
let sub_options = if let Some ( CommandDataOption {
value : CommandDataOptionValue ::SubCommand ( options ) ,
..
} ) = command . data . options . first ( )
{
options
} else {
return " Please provide sub options " . to_string ( ) ;
} ;
let code = if let Some ( x ) = sub_options . first ( ) {
match & x . value {
CommandDataOptionValue ::String ( y ) = > y . trim ( ) ,
_ = > return " Please provide a verification code " . to_string ( ) ,
}
} else {
return " Please provide a verification code " . to_string ( ) ;
} ;
db_pending_clear_expired ( & db ) . await ;
if details . auth_code ! = code {
return " Invalid verification code " . to_string ( ) ;
}
match db_pending_clear_successful ( & db , & command . user . id ) . await {
Ok ( _ ) = > {
return match set_discord ( & db , & command . user . id , & details . email ) . await {
Ok ( _ ) = > {
// get teh right roles for the user
set_server_roles ( & db , & command . user , ctx ) . await ;
// check if they are a committee member, and on that server
set_server_roles_committee ( & db , & command . user , ctx ) . await ;
" Discord username linked to Wolves " . to_string ( )
}
Err ( e ) = > {
println! ( " {:?} " , e ) ;
" Failed to save, please try /link_wolves again " . to_string ( )
}
} ;
}
Err ( e ) = > println! ( " {:?} " , e ) ,
}
" Failed to verify " . to_string ( )
}
async fn db_pending_clear_successful ( pool : & Pool < Sqlite > , user : & UserId ) -> Result < Option < WolvesVerify > , Error > {
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
DELETE
FROM wolves_verify
WHERE discord = ?
" #,
)
. bind ( user . get ( ) as i64 )
. fetch_optional ( pool )
. await
}
async fn set_discord ( db : & Pool < Sqlite > , discord : & UserId , email : & str ) -> Result < Option < Wolves > , Error > {
sqlx ::query_as ::< _ , Wolves > (
"
UPDATE wolves
SET discord = ?
WHERE email = ?
" ,
)
. bind ( discord . get ( ) as i64 )
. bind ( email )
. fetch_optional ( db )
. await
}
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 ( member ) = server . server . member ( & ctx . http , & discord . id ) . await {
if let Some ( config ) = get_server_config ( db , & server . server ) . await {
let Servers {
role_past ,
role_current ,
..
} = config ;
let mut roles = vec! [ ] ;
if let Some ( role ) = & role_past {
if ! member . roles . contains ( role ) {
roles . push ( role . to_owned ( ) ) ;
}
}
if ! member . roles . contains ( & role_current ) {
roles . push ( role_current . to_owned ( ) ) ;
}
if let Err ( e ) = member . add_roles ( & ctx , & roles ) . await {
println! ( " {:?} " , e ) ;
}
}
}
}
}
}
async fn get_committees_id ( db : & Pool < Sqlite > , wolves_id : i64 ) -> Vec < Committees > {
sqlx ::query_as ::< _ , Committees > (
r #"
SELECT *
FROM committees
WHERE committee LIKE ? 1
" #,
)
. bind ( format! ( " % {} % " , wolves_id ) )
. fetch_all ( db )
. await
. unwrap_or_else ( | e | {
dbg! ( e ) ;
vec! [ ]
} )
}
async fn set_server_roles_committee ( db : & Pool < Sqlite > , discord : & User , ctx : & Context ) {
2025-06-16 21:50:26 +01:00
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 ;
2025-03-06 20:43:36 +00:00
if let Some ( x ) = get_server_member_discord ( db , & discord . id ) . await {
// if they are a member of one or more committees, and in teh committee server then give the teh general committee role
// they will get teh more specific vanity role later
if ! get_committees_id ( db , x . id_wolves ) . await . is_empty ( ) {
2025-06-16 21:50:26 +01:00
let server = config . committee_server ;
let committee_member = config . committee_role ;
2025-03-06 20:43:36 +00:00
if let Ok ( member ) = server . member ( ctx , & discord . id ) . await {
member . add_roles ( & ctx , & [ committee_member ] ) . await . unwrap_or_default ( ) ;
}
}
}
}
async fn get_servers ( db : & Pool < Sqlite > , discord : & UserId ) -> Result < Vec < ServerMembersWolves > , Error > {
sqlx ::query_as ::< _ , ServerMembersWolves > (
"
SELECT *
FROM server_members
JOIN wolves USING ( id_wolves )
WHERE discord = ?
" ,
)
. bind ( discord . get ( ) as i64 )
. fetch_all ( db )
. await
}
}
pub mod unlink {
use serenity ::all ::{ CommandInteraction , Context , UserId } ;
use skynet_discord_bot ::common ::database ::{ DataBase , Wolves } ;
use sqlx ::{ Pool , Sqlite } ;
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 ( )
} ;
let db = db_lock . read ( ) . await ;
// dosent matter if there is one or not, it will be removed regardless
delete_link ( & db , & command . user . id ) . await ;
" Discord link removed " . to_string ( )
}
async fn delete_link ( db : & Pool < Sqlite > , user : & UserId ) {
match sqlx ::query_as ::< _ , Wolves > (
"
UPDATE wolves
SET discord = NULL
WHERE discord = ? 1 ;
" ,
)
. bind ( user . get ( ) as i64 )
. fetch_optional ( db )
. await
{
Ok ( _ ) = > { }
Err ( e ) = > {
dbg! ( e ) ;
}
}
}
}
pub fn register ( ) -> CreateCommand {
CreateCommand ::new ( " wolves " )
. description ( " Commands related to UL Wolves " )
// link
. add_option (
CreateCommandOption ::new ( CommandOptionType ::SubCommand , " link " , " Link your Wolves account to your Discord " )
. add_sub_option ( CreateCommandOption ::new ( CommandOptionType ::String , " email " , " UL Wolves Email " ) . required ( true ) ) ,
)
// verify
. add_option (
CreateCommandOption ::new ( CommandOptionType ::SubCommand , " verify " , " Verify Wolves Email " )
. add_sub_option ( CreateCommandOption ::new ( CommandOptionType ::String , " code " , " Code from verification email " ) . required ( true ) ) ,
)
// unlink
. add_option ( CreateCommandOption ::new ( CommandOptionType ::SubCommand , " unlink " , " Unlink your Wolves account from your Discord " ) )
2025-03-06 21:42:27 +00:00
. add_option (
CreateCommandOption ::new ( CommandOptionType ::SubCommand , " link_minecraft " , " Link your minecraft account " )
. add_sub_option ( CreateCommandOption ::new ( CommandOptionType ::String , " minecraft_username " , " Your Minecraft username " ) . required ( true ) )
. add_sub_option ( CreateCommandOption ::new ( CommandOptionType ::Boolean , " bedrock_account " , " Is this a Bedrock account? " ) . required ( false ) ) ,
)
2025-03-06 20:43:36 +00:00
}