2023-09-17 15:25:17 +01:00
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 ,
2023-10-02 09:22:52 +01:00
id ::UserId ,
2023-09-17 15:25:17 +01:00
prelude ::{ command ::CommandOptionType , interaction ::application_command ::CommandDataOptionValue } ,
} ,
} ;
use skynet_discord_bot ::{ get_now_iso , random_string , Config , DataBase , Wolves , WolvesVerify } ;
2023-09-16 22:47:26 +01:00
use sqlx ::{ Pool , Sqlite } ;
2024-03-03 14:40:37 +00:00
pub mod link {
2023-09-17 15:35:41 +01:00
use super ::* ;
pub async fn run ( command : & ApplicationCommandInteraction , 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 ;
2023-09-26 00:57:03 +01:00
if get_server_member_discord ( & db , & command . user . id ) . await . is_some ( ) {
2023-09-17 15:35:41 +01:00
return " Already linked " . to_string ( ) ;
}
2023-09-17 15:25:17 +01:00
2023-09-17 18:00:05 +01:00
db_pending_clear_expired ( & db ) . await ;
2023-09-28 18:25:03 +01:00
if get_verify_from_db ( & db , & command . user . id ) . await . is_some ( ) {
2023-09-17 18:00:05 +01:00
return " Linking already in process, please check email. " . to_string ( ) ;
}
2023-09-17 15:35:41 +01:00
let option = command
. data
. options
2024-05-06 02:12:26 +01:00
. first ( )
2023-09-17 15:35:41 +01:00
. expect ( " Expected email option " )
. resolved
. as_ref ( )
. expect ( " Expected email object " ) ;
let email = if let CommandDataOptionValue ::String ( email ) = option {
2023-09-18 09:52:02 +01:00
email . trim ( )
2023-09-17 15:35:41 +01:00
} else {
return " Please provide a valid user " . to_string ( ) ;
} ;
// check if email exists
let details = match get_server_member_email ( & db , email ) . await {
2023-09-18 09:52:02 +01:00
None = > {
2024-09-29 20:07:01 +01:00
return " Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up. " . to_string ( )
2023-09-18 09:52:02 +01:00
}
2023-09-17 15:35:41 +01:00
Some ( x ) = > x ,
} ;
2023-09-17 21:17:57 +01:00
if details . discord . is_some ( ) {
2023-09-17 15:35:41 +01:00
return " Email already verified " . to_string ( ) ;
}
2023-09-17 15:25:17 +01:00
2023-09-17 15:35:41 +01:00
// generate a auth key
let auth = random_string ( 20 ) ;
match send_mail ( & config , & details , & auth , & command . user . name ) {
2023-09-26 00:57:03 +01:00
Ok ( _ ) = > match save_to_db ( & db , & details , & auth , & command . user . id ) . await {
2023-09-17 15:35:41 +01:00
Ok ( _ ) = > { }
Err ( e ) = > {
return format! ( " Unable to save to db {} {e:?} " , & details . email ) ;
}
} ,
2023-09-17 15:25:17 +01:00
Err ( e ) = > {
2023-09-17 15:35:41 +01:00
return format! ( " Unable to send mail to {} {e:?} " , & details . email ) ;
2023-09-17 15:25:17 +01:00
}
}
2023-09-18 09:52:02 +01:00
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 )
2023-09-17 15:35:41 +01:00
}
2023-09-16 22:47:26 +01:00
2023-09-17 15:35:41 +01:00
pub fn register ( command : & mut CreateApplicationCommand ) -> & mut CreateApplicationCommand {
command
2024-03-03 13:04:10 +00:00
. name ( " link_wolves " )
2023-09-17 15:35:41 +01:00
. description ( " Set Wolves Email " )
. create_option ( | option | option . name ( " email " ) . description ( " UL Wolves Email " ) . kind ( CommandOptionType ::String ) . required ( true ) )
}
2023-09-16 22:47:26 +01:00
2024-03-03 13:59:23 +00:00
pub async fn get_server_member_discord ( db : & Pool < Sqlite > , user : & UserId ) -> Option < Wolves > {
2023-09-17 15:35:41 +01:00
sqlx ::query_as ::< _ , Wolves > (
r #"
2023-09-17 15:25:17 +01:00
SELECT *
FROM wolves
2023-09-17 18:00:05 +01:00
WHERE discord = ?
2023-09-17 15:25:17 +01:00
" #,
2023-09-17 15:35:41 +01:00
)
2023-09-26 00:57:03 +01:00
. bind ( * user . as_u64 ( ) as i64 )
2023-09-17 15:35:41 +01:00
. fetch_one ( db )
. await
. ok ( )
}
2023-09-17 15:25:17 +01:00
2023-09-17 15:35:41 +01:00
async fn get_server_member_email ( db : & Pool < Sqlite > , email : & str ) -> Option < Wolves > {
sqlx ::query_as ::< _ , Wolves > (
r #"
2023-09-17 00:14:50 +01:00
SELECT *
2023-09-17 15:25:17 +01:00
FROM wolves
WHERE email = ?
2023-09-17 00:14:50 +01:00
" #,
2023-09-17 15:35:41 +01:00
)
. bind ( email )
. fetch_one ( db )
. await
. ok ( )
}
2023-09-17 15:25:17 +01:00
2023-09-17 15:35:41 +01:00
fn send_mail ( config : & Config , email : & Wolves , auth : & str , user : & str ) -> Result < smtp ::response ::Response , smtp ::Error > {
let mail = & email . email ;
let discord = " https://discord.skynet.ie " ;
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 {
2023-09-17 22:04:46 +01:00
" Please use " pre { " /verify code: " ( auth ) } " to verify your discord account. "
2023-09-17 15:35:41 +01:00
}
p {
" If you have issues please refer to our Discord server: "
br ;
a href = ( discord ) { ( discord ) }
}
p {
" Skynet Team "
br ;
" UL Computer Society "
}
}
} ;
let body_text = format! (
r #"
2023-09-17 15:25:17 +01:00
Hi { user }
2023-09-17 22:04:46 +01:00
Please use " /verify code: {auth} " to verify your discord account .
2023-09-17 15:25:17 +01:00
If you have issues please refer to our Discord server :
{ discord }
Skynet Team
UL Computer Society
" #
2023-09-17 15:35:41 +01:00
) ;
// Build the message.
let email = Message ::builder ( )
. from ( sender . parse ( ) . unwrap ( ) )
. to ( mail . parse ( ) . unwrap ( ) )
. subject ( " Skynet-Discord: Link 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
2024-09-17 22:13:30 +01:00
let mailer = SmtpTransport ::starttls_relay ( & config . mail_smtp ) ? . credentials ( creds ) . build ( ) ;
2023-09-17 15:35:41 +01:00
// Send the email
mailer . send ( & email )
}
2023-09-17 15:25:17 +01:00
2023-09-17 18:00:05 +01:00
pub async fn db_pending_clear_expired ( pool : & Pool < Sqlite > ) -> Option < WolvesVerify > {
2023-09-17 15:35:41 +01:00
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
2023-09-17 15:25:17 +01:00
DELETE
FROM wolves_verify
WHERE date_expiry < ?
" #,
2023-09-17 15:35:41 +01:00
)
. bind ( get_now_iso ( true ) )
. fetch_one ( pool )
. await
. ok ( )
}
2023-09-17 15:25:17 +01:00
2023-09-28 18:25:03 +01:00
pub async fn get_verify_from_db ( db : & Pool < Sqlite > , user : & UserId ) -> Option < WolvesVerify > {
2023-09-17 15:35:41 +01:00
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
2023-09-17 15:25:17 +01:00
SELECT *
FROM wolves_verify
2023-09-17 18:00:05 +01:00
WHERE discord = ?
2023-09-17 15:25:17 +01:00
" #,
2023-09-17 15:35:41 +01:00
)
2023-09-28 18:25:03 +01:00
. bind ( * user . as_u64 ( ) as i64 )
2023-09-17 15:35:41 +01:00
. fetch_one ( db )
. await
. ok ( )
}
2023-09-17 15:25:17 +01:00
2023-09-26 00:57:03 +01:00
async fn save_to_db ( db : & Pool < Sqlite > , record : & Wolves , auth : & str , user : & UserId ) -> Result < Option < WolvesVerify > , sqlx ::Error > {
2023-09-17 15:35:41 +01:00
sqlx ::query_as ::< _ , WolvesVerify > (
"
2023-09-17 15:25:17 +01:00
INSERT INTO wolves_verify ( email , discord , auth_code , date_expiry )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 )
" ,
2023-09-17 15:35:41 +01:00
)
. bind ( record . email . to_owned ( ) )
2023-09-26 00:57:03 +01:00
. bind ( * user . as_u64 ( ) as i64 )
2023-09-17 15:35:41 +01:00
. bind ( auth . to_owned ( ) )
. bind ( get_now_iso ( false ) )
. fetch_optional ( db )
. await
}
2023-09-17 00:14:50 +01:00
}
2023-09-17 18:00:05 +01:00
2024-03-03 14:40:37 +00:00
pub mod verify {
2023-09-17 18:00:05 +01:00
use super ::* ;
use crate ::commands ::link_email ::link ::{ db_pending_clear_expired , get_verify_from_db } ;
2023-09-17 21:33:36 +01:00
use serenity ::model ::user ::User ;
use skynet_discord_bot ::{ get_server_config , ServerMembersWolves , Servers } ;
2023-09-17 18:00:05 +01:00
use sqlx ::Error ;
2023-09-17 21:33:36 +01:00
2023-09-17 18:00:05 +01:00
pub async fn run ( command : & ApplicationCommandInteraction , 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 ;
2024-03-02 21:45:43 +00:00
// check if user has used /link_wolves
2023-09-28 18:25:03 +01:00
let details = if let Some ( x ) = get_verify_from_db ( & db , & command . user . id ) . await {
2023-09-17 18:00:05 +01:00
x
} else {
2024-03-02 21:45:43 +00:00
return " Please use /link_wolves first " . to_string ( ) ;
2023-09-17 18:00:05 +01:00
} ;
let option = command
. data
. options
2024-05-06 02:12:26 +01:00
. first ( )
2023-09-17 18:00:05 +01:00
. 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 ( ) ;
} ;
db_pending_clear_expired ( & db ) . await ;
if & details . auth_code ! = code {
return " Invalid verification code " . to_string ( ) ;
}
2023-10-02 09:15:50 +01:00
match db_pending_clear_successful ( & db , & command . user . id ) . await {
2023-09-17 18:00:05 +01:00
Ok ( _ ) = > {
2023-10-02 09:15:50 +01:00
return match set_discord ( & db , & command . user . id , & details . email ) . await {
2023-09-17 21:33:36 +01:00
Ok ( _ ) = > {
// get teh right roles for the user
set_server_roles ( & db , & command . user , ctx ) . await ;
" Discord username linked to Wolves " . to_string ( )
}
2023-09-17 18:00:05 +01:00
Err ( e ) = > {
println! ( " {:?} " , e ) ;
2024-03-02 21:45:43 +00:00
" Failed to save, please try /link_wolves again " . to_string ( )
2023-09-17 18:00:05 +01:00
}
2023-09-17 21:33:36 +01:00
} ;
2023-09-17 18:00:05 +01:00
}
Err ( e ) = > println! ( " {:?} " , e ) ,
}
" 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 )
} )
}
2023-10-02 09:15:50 +01:00
async fn db_pending_clear_successful ( pool : & Pool < Sqlite > , user : & UserId ) -> Result < Option < WolvesVerify > , Error > {
2023-09-17 18:00:05 +01:00
sqlx ::query_as ::< _ , WolvesVerify > (
r #"
DELETE
FROM wolves_verify
WHERE discord = ?
" #,
)
2023-10-02 09:15:50 +01:00
. bind ( * user . as_u64 ( ) as i64 )
2023-09-17 18:00:05 +01:00
. fetch_optional ( pool )
. await
}
2023-10-02 09:15:50 +01:00
async fn set_discord ( db : & Pool < Sqlite > , discord : & UserId , email : & str ) -> Result < Option < Wolves > , Error > {
2023-09-17 18:00:05 +01:00
sqlx ::query_as ::< _ , Wolves > (
"
UPDATE wolves
SET discord = ?
WHERE email = ?
" ,
)
2023-10-02 09:15:50 +01:00
. bind ( * discord . as_u64 ( ) as i64 )
2023-09-17 18:00:05 +01:00
. bind ( email )
. fetch_optional ( db )
. await
}
2023-09-17 21:33:36 +01:00
async fn set_server_roles ( db : & Pool < Sqlite > , discord : & User , ctx : & Context ) {
2023-10-02 09:15:50 +01:00
if let Ok ( servers ) = get_servers ( db , & discord . id ) . await {
2023-09-17 21:33:36 +01:00
for server in servers {
if let Ok ( mut 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 ( ) ) ;
}
}
2024-09-17 22:08:20 +01:00
if ! member . roles . contains ( & role_current ) {
roles . push ( role_current . to_owned ( ) ) ;
2023-09-17 21:33:36 +01:00
}
if let Err ( e ) = member . add_roles ( & ctx , & roles ) . await {
println! ( " {:?} " , e ) ;
}
}
}
}
}
}
2023-10-02 09:15:50 +01:00
async fn get_servers ( db : & Pool < Sqlite > , discord : & UserId ) -> Result < Vec < ServerMembersWolves > , Error > {
2023-09-17 21:33:36 +01:00
sqlx ::query_as ::< _ , ServerMembersWolves > (
"
SELECT *
FROM server_members
JOIN wolves USING ( id_wolves )
WHERE discord = ?
" ,
)
2023-10-02 09:15:50 +01:00
. bind ( * discord . as_u64 ( ) as i64 )
2023-09-17 21:33:36 +01:00
. fetch_all ( db )
. await
}
2023-09-17 18:00:05 +01:00
}