add verify command

This commit is contained in:
Jack Merrill 2024-01-16 22:20:43 -06:00
parent a95b599bd3
commit e77b8b9f55
No known key found for this signature in database
GPG Key ID: B8E3CDF57DD80CA5
7 changed files with 331 additions and 0 deletions

View File

@ -41,6 +41,7 @@ WORKDIR /app
### Copy built binary application from 'builder' image
COPY --from=builder /app/main .
COPY --from=builder /app/verify.html .
### Copy the certs from builder
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

1
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/uuid v1.5.0
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jackmerrill/go-mapbox v0.4.5 // indirect
github.com/jackmerrill/hamp-api v0.0.0-20230818235104-8d222c9674c9 // indirect

2
go.sum
View File

@ -62,6 +62,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=

View File

@ -0,0 +1,270 @@
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()
err = SendEmail([]string{email}, code, ctx.GetUser())
if err != nil {
return err
}
// expires in 5 minutes
expires := time.Now().Add(time.Minute * 5)
m, err := ctx.GetSession().ChannelMessageSendComplex(ctx.GetChannel().ID, &discordgo.MessageSend{
Reference: ctx.GetMessage().Reference(),
Embed: embed.NewSuccessEmbed(ctx).SetTitle("Sent Verification Email").SetDescription("Waiting for you to verify...").AddField("Expires", fmt.Sprintf("<t:%d:R>", expires.Unix()), false).MessageEmbed,
})
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)
err = tmpl.Execute(buf, struct {
Code string
DiscordUserName string
}{
Code: code,
DiscordUserName: discordUser.Username,
})
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
}

View File

@ -27,6 +27,9 @@ var (
HampAPIHost = "api.hamp.sh"
// HampAPIHost = "localhost:1323"
// HampAPI = "http://localhost:1323"
VerifiedRoleId = "1020858770659745844"
StaffFacultyRoleId = "1183920480361648228"
)
var (

View File

@ -102,6 +102,10 @@ func main() {
handler.Register(&fun.MetricTime{})
log.Debug("Registered metrictime command")
handler.Register(&util.VerifyCommand{})
go util.StartWebserver(session)
log.Debug("Registered verify command")
log.Info("Registered all commands")
log.Info("Setting up activities...")

50
verify.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hampshire Hangout Email Verification</title>
<link
href="https://unpkg.com/tailwindcss@^3/dist/tailwind.min.css"
rel="stylesheet"
/>
</head>
<body>
<div
class="flex items-center justify-center min-h-screen p-5 bg-gray-900 min-w-screen"
>
<div
class="max-w-xl p-8 text-center text-gray-800 bg-white shadow-xl lg:max-w-3xl rounded-3xl lg:p-12"
>
<h3 class="text-2xl font-bold">Hampshire Hangout Email Verification</h3>
<p class="mt-4 text-sm">
Thanks for joining Hampshire Hangout, {{ .DiscordUserName }}! We're
excited to have you join our community. Before you can start using
your account, you need to verify your email address by clicking the
button below:
</p>
<div class="mt-4">
<a
class="px-2 py-2 text-white bg-blue-600 rounded-md font-semibold"
href="https://hampbot.fly.dev/verify?code={{ .Code }}"
>Click to Verify Email</a
>
<p class="mt-4 text-sm">
If you're having trouble clicking the "Verify Email Address" button,
copy and paste the URL below into your web browser:
<a
href="https://hampbot.fly.dev/verify?code={{ .Code }}"
class="text-blue-600"
>https://hampbot.fly.dev/verify?code={{ .Code }}</a
>
</p>
</div>
<p class="text-sm mt-4 text-gray-600">
If you didn't request this, ignore it!
</p>
</div>
</div>
</body>
</html>