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 ;
2025-02-19 00:17:02 +00:00
use serenity ::{ builder ::CreateCommand , client ::Context , model ::id ::UserId } ;
2024-10-28 21:53:04 +00:00
use skynet_discord_bot ::common ::database ::{ DataBase , Wolves , WolvesVerify } ;
2024-10-28 00:59:04 +00:00
use skynet_discord_bot ::{ get_now_iso , random_string , Config } ;
2023-09-16 22:47:26 +01:00
use sqlx ::{ Pool , Sqlite } ;
2024-10-28 00:59:04 +00:00
2024-03-03 14:40:37 +00:00
pub mod link {
2023-09-17 15:35:41 +01:00
use super ::* ;
2024-11-09 02:23:46 +00:00
use serde ::{ Deserialize , Serialize } ;
2025-02-19 00:17:02 +00:00
use serenity ::all ::{ CommandDataOption , CommandDataOptionValue , CommandInteraction , CommandOptionType , CreateCommand , CreateCommandOption } ;
2023-09-17 15:35:41 +01:00
2025-02-19 00:17:02 +00:00
pub async fn run ( command : & CommandInteraction , ctx : & Context ) -> String {
2023-09-17 15:35:41 +01:00
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 ( ) ;
}
2025-02-19 00:17:02 +00:00
let email = if let Some ( CommandDataOption {
value : CommandDataOptionValue ::String ( email ) ,
..
} ) = command . data . options . first ( )
{
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-11-09 02:23:46 +00:00
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 ( ) ;
2024-11-23 21:53:30 +00:00
let wolves = wolves_oxidised ::Client ::new ( & config . wolves_url , Some ( & config . wolves_api ) ) ;
2024-11-09 02:23:46 +00:00
// see if the user actually exists
2024-11-23 21:53:30 +00:00
let id = match wolves . get_member ( email ) . await {
2024-11-09 02:23:46 +00:00
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 ,
}
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
2025-02-19 00:17:02 +00:00
pub fn register ( ) -> CreateCommand {
CreateCommand ::new ( " link_wolves " )
2023-09-17 15:35:41 +01:00
. description ( " Set Wolves Email " )
2025-02-19 00:17:02 +00:00
. add_option ( CreateCommandOption ::new ( CommandOptionType ::String , " email " , " UL Wolves Email " ) . required ( true ) )
2023-09-17 15:35:41 +01:00
}
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
)
2025-02-19 00:17:02 +00:00
. bind ( user . get ( ) 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; } "
}
}
2024-11-18 16:09:43 +00:00
div {
2023-09-17 15:35:41 +01:00
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
)
2025-02-19 00:17:02 +00:00
. bind ( user . get ( ) 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 ( ) )
2025-02-19 00:17:02 +00:00
. bind ( user . get ( ) as i64 )
2023-09-17 15:35:41 +01:00
. bind ( auth . to_owned ( ) )
. bind ( get_now_iso ( false ) )
. fetch_optional ( db )
. await
}
2024-11-09 02:23:46 +00:00
#[ 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 > (
"
INSERT INTO wolves ( id_wolves , email )
2024-11-09 12:53:53 +00:00
VALUES ( $ 1 , $ 2 )
ON CONFLICT ( id_wolves ) DO UPDATE SET email = $ 2
2024-11-09 02:23:46 +00:00
" ,
)
. bind ( id_wolves )
. bind ( email )
. 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 ::* ;
2025-02-19 22:36:39 +00:00
use crate ::commands ::link_email ::link ::{ db_pending_clear_expired , get_server_member_discord , get_verify_from_db } ;
use serenity ::all ::{ CommandDataOption , CommandDataOptionValue , CommandInteraction , CommandOptionType , CreateCommandOption , GuildId , RoleId } ;
2023-09-17 21:33:36 +01:00
use serenity ::model ::user ::User ;
2024-10-28 00:59:04 +00:00
use skynet_discord_bot ::common ::database ::get_server_config ;
use skynet_discord_bot ::common ::database ::{ ServerMembersWolves , Servers } ;
2025-02-19 22:36:39 +00:00
use skynet_discord_bot ::common ::wolves ::committees ::Committees ;
2024-10-28 21:53:04 +00:00
use sqlx ::Error ;
2023-09-17 21:33:36 +01:00
2025-02-19 00:17:02 +00:00
pub async fn run ( command : & CommandInteraction , ctx : & Context ) -> String {
2023-09-17 18:00:05 +01:00
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
} ;
2025-02-19 00:17:02 +00:00
let code = if let Some ( CommandDataOption {
value : CommandDataOptionValue ::String ( code ) ,
..
} ) = command . data . options . first ( )
{
2023-09-17 18:00:05 +01:00
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 ;
2025-02-19 22:36:39 +00:00
// check if they are a committee member, and on that server
set_server_roles_committee ( & db , & command . user , ctx ) . await ;
2023-09-17 21:33:36 +01:00
" 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 ( )
}
2025-02-19 00:17:02 +00:00
pub fn register ( ) -> CreateCommand {
CreateCommand ::new ( " verify " )
. description ( " Verify Wolves Email " )
. add_option ( CreateCommandOption ::new ( CommandOptionType ::String , " code " , " Code from verification email " ) . required ( true ) )
2023-09-17 18:00:05 +01:00
}
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 = ?
" #,
)
2025-02-19 00:17:02 +00:00
. bind ( user . get ( ) 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 = ?
" ,
)
2025-02-19 00:17:02 +00:00
. bind ( discord . get ( ) 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 {
2025-02-19 00:17:02 +00:00
if let Ok ( member ) = server . server . member ( & ctx . http , & discord . id ) . await {
2023-09-17 21:33:36 +01:00
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 ) ;
}
}
}
}
}
}
2025-02-19 22:36:39 +00:00
async fn get_committees_id ( db : & Pool < Sqlite > , wolves_id : i64 ) -> Vec < Committees > {
sqlx ::query_as ::< _ , Committees > (
r #"
SELECT *
2025-02-25 17:34:33 +00:00
FROM committees
WHERE committee LIKE ? 1
2025-02-19 22:36:39 +00:00
" #,
)
2025-02-25 17:34:33 +00:00
. bind ( format! ( " % {} % " , wolves_id ) )
2025-02-19 22:36:39 +00:00
. fetch_all ( db )
. await
. unwrap_or_else ( | e | {
dbg! ( e ) ;
vec! [ ]
} )
}
async fn set_server_roles_committee ( db : & Pool < Sqlite > , discord : & User , ctx : & Context ) {
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 ( ) {
let server = GuildId ::new ( 1220150752656363520 ) ;
let committee_member = RoleId ::new ( 1226602779968274573 ) ;
if let Ok ( member ) = server . member ( ctx , & discord . id ) . await {
member . add_roles ( & ctx , & [ committee_member ] ) . await . unwrap_or_default ( ) ;
}
}
}
}
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 = ?
" ,
)
2025-02-19 00:17:02 +00:00
. bind ( discord . get ( ) as i64 )
2023-09-17 21:33:36 +01:00
. fetch_all ( db )
. await
}
2023-09-17 18:00:05 +01:00
}