2024-01-16 20:20:43 -08:00
package commands
import (
"bytes"
"fmt"
"html/template"
"net/http"
"net/smtp"
"os"
"regexp"
"strings"
"time"
"github.com/bwmarrin/discordgo"
"github.com/google/uuid"
"github.com/jackmerrill/hampbot/internal/utils/config"
"github.com/jackmerrill/hampbot/internal/utils/embed"
"github.com/zekroTJA/shireikan"
)
type VerifyCommand struct {
}
func ( c * VerifyCommand ) GetInvokes ( ) [ ] string {
return [ ] string { "verify" }
}
func ( c * VerifyCommand ) GetDescription ( ) string {
return "Verify your student/staff/faculty status with your school email."
}
func ( c * VerifyCommand ) GetHelp ( ) string {
return "`verify` - `verify [email]`"
}
func ( c * VerifyCommand ) GetGroup ( ) string {
return config . GroupUtil
}
func ( c * VerifyCommand ) GetDomainName ( ) string {
return "hamp.util.verify"
}
func ( c * VerifyCommand ) GetSubPermissionRules ( ) [ ] shireikan . SubPermission {
return nil
}
func ( c * VerifyCommand ) IsExecutableInDMChannels ( ) bool {
return true
}
type Verification struct {
User * discordgo . User
Email string
Expires time . Time
IsStaffFaculty bool
}
var VerificationCodes map [ string ] Verification = make ( map [ string ] Verification )
func ( c * VerifyCommand ) Exec ( ctx shireikan . Context ) error {
// first check if the user is already verified (has the role)
guildMember , err := ctx . GetSession ( ) . GuildMember ( config . BotGuild , ctx . GetUser ( ) . ID )
if err != nil {
return err
}
for _ , role := range guildMember . Roles {
if role == config . VerifiedRoleId {
ctx . GetSession ( ) . ChannelMessageSendComplex ( ctx . GetChannel ( ) . ID , & discordgo . MessageSend {
Reference : ctx . GetMessage ( ) . Reference ( ) ,
Embed : embed . NewErrorEmbed ( ctx ) . SetTitle ( "Already Verified" ) . SetDescription ( "You are already verified." ) . MessageEmbed ,
} )
return nil
}
}
// first arg is the email
email := ctx . GetArgs ( ) . Get ( 0 ) . AsString ( )
// check if the email is valid, and if it is a hampshire.edu email
if ! strings . Contains ( email , "@hampshire.edu" ) {
ctx . GetSession ( ) . ChannelMessageSendComplex ( ctx . GetChannel ( ) . ID , & discordgo . MessageSend {
Reference : ctx . GetMessage ( ) . Reference ( ) ,
Embed : embed . NewErrorEmbed ( ctx ) . SetTitle ( "Invalid Email" ) . SetDescription ( "Please use a valid @hampshire.edu email." ) . MessageEmbed ,
} )
return nil
}
code := uuid . New ( ) . String ( )
2024-09-22 14:55:46 -07:00
m , err := ctx . GetSession ( ) . ChannelMessageSendComplex ( ctx . GetChannel ( ) . ID , & discordgo . MessageSend {
Reference : ctx . GetMessage ( ) . Reference ( ) ,
Embed : embed . NewWarningEmbed ( ctx ) . SetTitle ( "Sending Verification Email" ) . SetDescription ( "Please wait..." ) . MessageEmbed ,
} )
2024-01-16 20:20:43 -08:00
err = SendEmail ( [ ] string { email } , code , ctx . GetUser ( ) )
if err != nil {
return err
}
// expires in 5 minutes
expires := time . Now ( ) . Add ( time . Minute * 5 )
2024-09-22 14:55:46 -07:00
ctx . GetSession ( ) . ChannelMessageEditComplex ( & discordgo . MessageEdit {
ID : m . ID ,
Channel : ctx . GetChannel ( ) . ID ,
2024-09-22 15:02:48 -07:00
Embed : embed . NewSuccessEmbed ( ctx ) . SetTitle ( "Sent Verification Email" ) . SetDescription ( "Waiting for you to verify...\n\n**Make sure to check your spam folder!**" ) . AddField ( "Expires" , fmt . Sprintf ( "<t:%d:R>" , expires . Unix ( ) ) , false ) . MessageEmbed ,
2024-01-16 20:20:43 -08:00
} )
if err != nil {
return err
}
// check if the email is student or staff/faculty
// student emails are in the format of their initials plus their first year (e.g. jmm18). To be safe, lets allow up to six initials plus the year (e.g. jmmmmmm18)
// staff/faculty emails are in the format of their first initial plus their last name (e.g. jsmith), or their first and last initial plus their department (e.g. jsIA)
hampnetUser := strings . Split ( email , "@" ) [ 0 ]
// use regex to check
// student
if match , _ := regexp . MatchString ( ` ^[a-z] { 1,6}[0-9] { 2}$ ` , hampnetUser ) ; match {
VerificationCodes [ code ] = Verification {
User : ctx . GetUser ( ) ,
Email : email ,
Expires : expires ,
IsStaffFaculty : false ,
}
} else if match , _ := regexp . MatchString ( ` ^[a-z] { 1,2}[a-z] { 1,2}[a-z] { 1,2}[a-z] { 1,2}$ ` , hampnetUser ) ; match {
VerificationCodes [ code ] = Verification {
User : ctx . GetUser ( ) ,
Email : email ,
Expires : expires ,
IsStaffFaculty : true ,
}
} else {
// potentially student, but not in the format of a student email
VerificationCodes [ code ] = Verification {
User : ctx . GetUser ( ) ,
Email : email ,
Expires : expires ,
IsStaffFaculty : false ,
}
}
// wait for the user to verify
for {
if time . Now ( ) . After ( expires ) {
ctx . GetSession ( ) . ChannelMessageEditComplex ( & discordgo . MessageEdit {
ID : m . ID ,
Channel : ctx . GetChannel ( ) . ID ,
Embed : embed . NewErrorEmbed ( ctx ) . SetTitle ( "Verification Expired" ) . SetDescription ( "Please try again." ) . MessageEmbed ,
} )
}
_ , ok := VerificationCodes [ code ]
if ! ok {
ctx . GetSession ( ) . ChannelMessageEditComplex ( & discordgo . MessageEdit {
ID : m . ID ,
Channel : ctx . GetChannel ( ) . ID ,
Embed : embed . NewSuccessEmbed ( ctx ) . SetTitle ( "Verified!" ) . SetDescription ( "You can access the server now." ) . MessageEmbed ,
} )
break
}
time . Sleep ( time . Second * 5 )
}
return nil
}
func StartWebserver ( session * discordgo . Session ) {
// listen to /verify
http . HandleFunc ( "/verify" , func ( w http . ResponseWriter , r * http . Request ) {
code := r . URL . Query ( ) . Get ( "code" )
if code == "" {
http . Error ( w , "Code is missing" , http . StatusBadRequest )
return
}
v , ok := VerificationCodes [ code ]
if ! ok {
http . Error ( w , "Code is invalid" , http . StatusBadRequest )
return
}
err := session . GuildMemberRoleAdd ( config . BotGuild , v . User . ID , config . VerifiedRoleId )
if err != nil {
http . Error ( w , "Failed to add role" , http . StatusInternalServerError )
return
}
// remove the code from the map
delete ( VerificationCodes , code )
fmt . Fprintf ( w , "Verified! You can close this tab now." )
} )
http . HandleFunc ( "/" , func ( w http . ResponseWriter , r * http . Request ) {
fmt . Fprintf ( w , "OK!" )
} )
http . ListenAndServe ( ":8080" , nil )
}
func SendEmail ( to [ ] string , code string , discordUser * discordgo . User ) error {
sender := "automated@jackmerrill.com"
user := os . Getenv ( "SMTP_USER" )
password := os . Getenv ( "SMTP_PASSWORD" )
subject := "Hampshire Hangout Verification"
tmpl , err := template . ParseFiles ( "verify.html" )
if err != nil {
return err
}
buf := new ( bytes . Buffer )
2024-09-22 14:55:46 -07:00
data := struct {
2024-01-16 20:20:43 -08:00
Code string
DiscordUserName string
2024-09-22 14:55:46 -07:00
BaseUrl string
2024-01-16 20:20:43 -08:00
} {
Code : code ,
DiscordUserName : discordUser . Username ,
2024-09-22 14:55:46 -07:00
BaseUrl : "http://localhost:8080" ,
}
if os . Getenv ( "ENV" ) != "development" {
data . BaseUrl = "https://hampbot.fly.dev"
}
err = tmpl . Execute ( buf , data )
2024-01-16 20:20:43 -08:00
if err != nil {
return err
}
request := Mail {
Sender : sender ,
To : to ,
Subject : subject ,
Body : buf . String ( ) ,
}
addr := "smtp.gmail.com:587"
host := "smtp.gmail.com"
msg := BuildMessage ( request )
auth := smtp . PlainAuth ( "" , user , password , host )
err = smtp . SendMail ( addr , auth , sender , to , [ ] byte ( msg ) )
if err != nil {
return err
}
return nil
}
func BuildMessage ( mail Mail ) string {
msg := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n"
msg += fmt . Sprintf ( "From: %s\r\n" , mail . Sender )
msg += fmt . Sprintf ( "To: %s\r\n" , strings . Join ( mail . To , ";" ) )
msg += fmt . Sprintf ( "Subject: %s\r\n" , mail . Subject )
msg += fmt . Sprintf ( "\r\n%s\r\n" , mail . Body )
return msg
}
type Mail struct {
Sender string
To [ ] string
Subject string
Body string
}