diff --git a/apps/api/.env.dev.example b/apps/api/.env.dev.example
index 66b5994b..71119c07 100644
--- a/apps/api/.env.dev.example
+++ b/apps/api/.env.dev.example
@@ -13,6 +13,9 @@ AUTH_DISCORD_CLIENT_ID=
AUTH_DISCORD_CLIENT_SECRET=
AUTH_DISCORD_REDIRECT_URI="http://localhost:8080/auth/callback"
+# CF
+CORE_BUCKETS_USER_QRCODES_BASE_URL=
+
# For cookies
COOKIE_DOMAIN=localhost
COOKIE_SECURE=false
diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go
index 3ebc57ef..e745ce78 100644
--- a/apps/api/cmd/BAT_worker/main.go
+++ b/apps/api/cmd/BAT_worker/main.go
@@ -82,7 +82,7 @@ func main() {
batRunsRepo := repository.NewBatRunsRepository(database)
sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
- emailService := services.NewEmailService(taskQueueClient, sesClient, logger)
+ emailService := services.NewEmailService(taskQueueClient, sesClient, nil, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, logger)
applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, nil, nil, scheduler, logger)
diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go
index 73973301..1e427e1f 100644
--- a/apps/api/cmd/api/main.go
+++ b/apps/api/cmd/api/main.go
@@ -89,7 +89,7 @@ func main() {
userService := services.NewUserService(userRepo, logger)
eventInterestService := services.NewEventInterestService(eventInterestRepo, logger)
eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger)
- emailService := services.NewEmailService(taskQueueClient, sesClient, logger)
+ emailService := services.NewEmailService(taskQueueClient, sesClient, r2Client, logger)
applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, nil, logger)
teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, logger)
diff --git a/apps/api/cmd/email_worker/main.go b/apps/api/cmd/email_worker/main.go
index d03da5bb..6ee0ae17 100644
--- a/apps/api/cmd/email_worker/main.go
+++ b/apps/api/cmd/email_worker/main.go
@@ -41,7 +41,7 @@ func main() {
// Create ses client
sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
- emailService := services.NewEmailService(nil, sesClient, logger)
+ emailService := services.NewEmailService(nil, sesClient, nil, logger)
emailWorker := workers.NewEmailWorker(emailService, logger)
mux := asynq.NewServeMux()
diff --git a/apps/api/go.mod b/apps/api/go.mod
index f36f77a1..de09374e 100644
--- a/apps/api/go.mod
+++ b/apps/api/go.mod
@@ -63,6 +63,7 @@ require (
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/redis/go-redis/v9 v9.7.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/sv-tools/openapi v0.4.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
diff --git a/apps/api/go.sum b/apps/api/go.sum
index 207cfa61..60638828 100644
--- a/apps/api/go.sum
+++ b/apps/api/go.sum
@@ -133,6 +133,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go
index 610e3553..2a8dc92f 100644
--- a/apps/api/internal/api/api.go
+++ b/apps/api/internal/api/api.go
@@ -155,6 +155,8 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
+ r.With(ensureEventAdmin).Post("/queue-welcome-email", api.Handlers.Email.QueueWelcomeEmail)
+ r.With(ensureEventAdmin).Post("/send-welcome-emails", api.Handlers.Bat.SendWelcomeEmails)
r.With(ensureEventAdmin).Post("/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest)
r.With(ensureEventAdmin).Patch("/transition-waitlisted-applications", api.Handlers.Application.TransitionWaitlistedApplications)
r.With(ensureEventAdmin).Post("/begin-waitlist-transition", api.Handlers.Bat.QueueScheduleWaitlistTransitionTask)
diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go
index 915b9d4b..fe4ee26b 100644
--- a/apps/api/internal/api/handlers/bat.go
+++ b/apps/api/internal/api/handlers/bat.go
@@ -197,3 +197,29 @@ func (h *BatHandler) QueueShutdownWaitlistSchedulerTask(w http.ResponseWriter, r
res.Send(w, http.StatusOK, nil)
}
+
+// Send welcome emails
+//
+// @Summary Sends welcome emails to attendees
+// @Description
+// @Tags
+//
+// @Param eventId path string true "ID of the event"
+// @Success 200 "Welcome emails began to queue successfully"
+// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
+// @Failure 500 {object} res.ErrorResponse "Server error: failed to begin queuing welcome emails"
+// @Router /events/{eventId}/send-welcome-emails [post]
+func (h *BatHandler) SendWelcomeEmails(w http.ResponseWriter, r *http.Request) {
+ eventId, err := web.PathParamToUUID(r, "eventId")
+ if err != nil {
+ res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid."))
+ return
+ }
+
+ err = h.BatService.SendWelcomeEmailToAttendees(r.Context(), eventId)
+ if err != nil {
+ res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to create ScheduleWaitlistTransition task."))
+ }
+
+ res.Send(w, http.StatusCreated, nil)
+}
diff --git a/apps/api/internal/api/handlers/email.go b/apps/api/internal/api/handlers/email.go
index d0639bdf..32947389 100644
--- a/apps/api/internal/api/handlers/email.go
+++ b/apps/api/internal/api/handlers/email.go
@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/go-playground/validator/v10"
+ "github.com/google/uuid"
"github.com/rs/zerolog"
res "github.com/swamphacks/core/apps/api/internal/api/response"
"github.com/swamphacks/core/apps/api/internal/email"
@@ -82,6 +83,18 @@ type QueueConfirmationEmailFields struct {
FirstName string `json:"firstName" validate:"required"`
}
+// Queue a Confirmation Email
+//
+// @Summary Queue a Confirmation Email Request
+// @Description Push a Confirmation Email request to the task queue
+// @Tags Email
+// @Accept json
+// @Produce json
+// @Param request body QueueConfirmationEmailFields true "Email data"
+// @Success 201 {object} string "OK: Email request queued"
+// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid."
+// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending"
+// @Router /email/queue [post]
func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) {
var req QueueConfirmationEmailFields
decoder := json.NewDecoder(r.Body)
@@ -103,3 +116,49 @@ func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Req
res.Send(w, http.StatusOK, nil)
}
+
+type QueueWelcomeEmailFields struct {
+ Email string `json:"email" validate:"required"`
+ FirstName string `json:"firstName" validate:"required"`
+ UserId string `json:userId validate:"required"`
+}
+
+// Queue a Welcome Email
+//
+// @Summary Queue a Welcome Email
+// @Description Push an Welcome Email request to the task queue
+// @Tags Email
+// @Accept json
+// @Produce json
+// @Param request body QueueConfirmationEmailFields true "Email data"
+// @Success 201 {object} string "OK: Email request queued"
+// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid."
+// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending"
+func (h *EmailHandler) QueueWelcomeEmail(w http.ResponseWriter, r *http.Request) {
+ var req QueueWelcomeEmailFields
+ decoder := json.NewDecoder(r.Body)
+ decoder.DisallowUnknownFields()
+ err := decoder.Decode(&req)
+ if err != nil {
+ res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body"))
+ return
+ }
+
+ validate := validator.New()
+ if err := validate.Struct(req); err != nil {
+ res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error()))
+ }
+
+ parsedUserId, err := uuid.Parse(req.UserId)
+ if err != nil {
+ res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "userId must be of type uuid"))
+ return
+ }
+
+ err = h.emailService.QueueWelcomeEmail(r.Context(), req.Email, req.FirstName, parsedUserId)
+ if err != nil {
+ res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Hacker email could not be queued."))
+ }
+
+ res.Send(w, http.StatusOK, nil)
+}
diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go
index fbb74fde..be937a86 100644
--- a/apps/api/internal/config/config.go
+++ b/apps/api/internal/config/config.go
@@ -48,9 +48,11 @@ type AWSConfig struct {
type CoreBuckets struct {
Avatars string `env:"USER_AVATARS" envDefault:"core-user-avatars-dev"`
+ QRCodes string `env:"USER_QRCODES" envDefault:"core-user-qrcodes-dev"`
ApplicationResumes string `env:"APPLICATION_RESUMES" envDefault:"core-application-resumes-dev"`
EventAssets string `env:"EVENT_ASSETS" envDefault:"core-event-assets-dev"`
AvatarsBaseUrl string `env:"USER_AVATARS_BASE_URL"`
+ QRCodesBaseUrl string `env:"USER_QRCODES_BASE_URL"`
EventAssetsBaseUrl string `env:"EVENT_ASSETS_BASE_URL"`
}
diff --git a/apps/api/internal/db/queries/event_roles.sql b/apps/api/internal/db/queries/event_roles.sql
index 12b8bf59..a68e0594 100644
--- a/apps/api/internal/db/queries/event_roles.sql
+++ b/apps/api/internal/db/queries/event_roles.sql
@@ -60,9 +60,14 @@ SELECT COUNT(*) FROM event_roles AS er
WHERE er.event_id = @event_id::uuid
AND er.role = 'attendee';
+-- name: GetAttendeeUserIdsByEventId :many
+SELECT er.user_id FROM event_roles AS er
+WHERE er.event_id = @event_id::uuid
+ AND er.role = 'attendee';
+
-- name: GetUserByRFID :one
SELECT u.*
FROM auth.users u
JOIN event_roles er ON u.id = er.user_id
WHERE er.event_id = $1
- AND er.rfid = $2;
\ No newline at end of file
+ AND er.rfid = $2;
diff --git a/apps/api/internal/db/repository/events.go b/apps/api/internal/db/repository/events.go
index 43a4a005..86ed536e 100644
--- a/apps/api/internal/db/repository/events.go
+++ b/apps/api/internal/db/repository/events.go
@@ -204,4 +204,8 @@ func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context,
}
return &eventRole, nil
-}
\ No newline at end of file
+}
+
+func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) {
+ return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID)
+}
diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go
index b32d9789..32c99a23 100644
--- a/apps/api/internal/db/sqlc/event_roles.sql.go
+++ b/apps/api/internal/db/sqlc/event_roles.sql.go
@@ -42,6 +42,32 @@ func (q *Queries) GetAttendeeCountByEventId(ctx context.Context, eventID uuid.UU
return count, err
}
+const getAttendeeUserIdsByEventId = `-- name: GetAttendeeUserIdsByEventId :many
+SELECT er.user_id FROM event_roles AS er
+WHERE er.event_id = $1::uuid
+ AND er.role = 'attendee'
+`
+
+func (q *Queries) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) {
+ rows, err := q.db.Query(ctx, getAttendeeUserIdsByEventId, eventID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []uuid.UUID{}
+ for rows.Next() {
+ var user_id uuid.UUID
+ if err := rows.Scan(&user_id); err != nil {
+ return nil, err
+ }
+ items = append(items, user_id)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const getEventAttendeesWithDiscord = `-- name: GetEventAttendeesWithDiscord :many
SELECT
a.account_id as discord_id,
diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/email/templates/WelcomeEmail.html
new file mode 100644
index 00000000..cba2022b
--- /dev/null
+++ b/apps/api/internal/email/templates/WelcomeEmail.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+ Welcome to SwampHacks XI!
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ Hello {{ .Name }},
+ |
+
+
+
+
+ |
+
+ Our names are Elle and Rob, we’re SwampHacks XI’s organizers and on behalf of the University of Florida,
+ we’re so excited to welcome you to 36 hours of hell hacking! (Though after a couple hours of
+ debugging
+ at 3 AM, you might not notice the difference.)
+
+
+
+ With SwampHacks at the end of the week, we wanted to share some important information and reminders:
+
+
+
+
+ Check out our Hacker Guide! This is
+ essential—your full access to venue, schedule, meals, tracks, challenges, submission, judging, FAQs,
+ and emergency contacts depends on reviewing this guide before arrival.
+
+
+
+ Before the Event
+
+ - We understand that things happen. If you are no longer able to attend, please fill out this
+ form so that we can offer your spot to someone that can attend.
+ - During the event we’ll be using our Discord server to update you when food, workshops, events occur
+ and of any important information (project submission details, judging info, etc.). Make sure
+ you have
+ joined our Discord server (you want to join so badd 🌀)!
+
+ - Still looking for a team? Check out our
#🔍│looking-for-a-team
+ channel in Discord. If you can't find
+ a team ahead of our event, we’re going to have a hacker meet up (in Little Hall, LIT 113) right after
+ the opening ceremony to help you find a team!
+ - Visiting from outside of Gainesville? Newell Hall will be open 24/7 for those of you
+ visiting us
+ this weekend. The 3rd and 4th floors of Newell will be designated “quiet spaces” starting when Little
+ Hall closes at 10 PM to breakfast at 8 AM. In alignment with our commitment to an equitable experience
+ for all, non-UF attendees will be given priority access to our overnight venue. That being said, if
+ you’re visiting us, please make sure to bring along toiletries and such (see our Hacker guide for our
+ recommended list and details regarding access to showers, or click here).
+ - Feel free to post that you are attending SwampHacks XI with our attached graphic (special thanks to
+ our marketing team).
+
+
+ Check-in & Day Of
+
+ - To attend, you must check in (see QR code below) by 6:30 pm at LIT 113 or fill out our
+ late-arrival
+ form by 6 pm, Friday
+ to ensure
+ that we don’t give your spot away to those in the in-person waitlist. At check-in, you will present
+ your check-in QR code found below and receive the badge you will use to guarantee you access to venue,
+ food, and events. The QR code can also be found in the portal.
+
+
+
+ 
+
+
+
+ - Last thing, opening ceremony begins at 6:30 PM in Carleton Auditorium, right across from our
+ check-in room. If you’ve confirmed that you’re arriving late, go straight to Carleton for the opening
+ ceremony. Right before the opening ceremony, we will be taking note of who’s in our waitlist queue,
+ accepting those who we can, and temporarily closing the check-in room. Check-in will re-open in LIT
+ 113 for confirmed late arrivals and for people we accepted off the waitlist after the ceremony
+ concludes.
+
+
+
+ Have questions for us? Feel free to reach out to contact@swamphacks.com or ask in #🪪│ask-staff
+ for a quicker response.
+
+
+
+ We look forward to meeting you and seeing what you’ve got!
+
+
+
+ Best regards,
+
+ Elle and Robert
+ Organizers, SwampHacks XI
+ Department of Computer & Information Science & Engineering
+ University of Florida
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go
index c2860e09..63304162 100644
--- a/apps/api/internal/services/bat.go
+++ b/apps/api/internal/services/bat.go
@@ -149,7 +149,10 @@ func (s *BatService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun)
if !ok {
return ErrFailedToGetContactEmail
}
- taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailInfo.Name, accepetedEmailTemplatePath)
+ type emailTemplateData struct {
+ Name string
+ }
+ taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailTemplateData{Name: emailInfo.Name}, accepetedEmailTemplatePath)
s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued acceptance email")
}
@@ -163,7 +166,10 @@ func (s *BatService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun)
if !ok {
return ErrFailedToGetContactEmail
}
- taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailInfo.Name, rejectedEmailTemplatePath)
+ type emailTemplateData struct {
+ Name string
+ }
+ taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailTemplateData{emailInfo.Name}, rejectedEmailTemplatePath)
s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued rejection email")
}
@@ -435,3 +441,32 @@ func (s *BatService) QueueShutdownWaitlistScheduler() error {
return nil
}
+
+func (s *BatService) SendWelcomeEmailToAttendees(ctx context.Context, eventId uuid.UUID) error {
+ attendees, err := s.eventRepo.GetAttendeeUserIdsByEventId(ctx, eventId)
+ if err != nil {
+ s.logger.Err(err).Msg("Could not get attendee user ids")
+ return err
+ }
+
+ s.logger.Info().Msgf("Sending welcome emails to %v attendees", len(attendees))
+
+ for _, userId := range attendees {
+ contactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userId)
+ if err != nil {
+ s.logger.Err(err).Msgf("Could not get contact info for user with id %s", userId)
+ return err
+ }
+ contactEmail, ok := contactInfo.ContactEmail.(string)
+ if !ok {
+ return ErrFailedToGetContactEmail
+ }
+
+ err = s.emailService.QueueWelcomeEmail(ctx, contactEmail, contactInfo.Name, userId)
+ if err != nil {
+ s.logger.Err(err).Msgf("Could not queue welcome email for user with id %s", userId)
+ return err
+ }
+ }
+ return nil
+}
diff --git a/apps/api/internal/services/email.go b/apps/api/internal/services/email.go
index 23196980..462e8bab 100644
--- a/apps/api/internal/services/email.go
+++ b/apps/api/internal/services/email.go
@@ -2,26 +2,33 @@ package services
import (
"bytes"
+ "context"
+ "fmt"
"html/template"
+ "github.com/google/uuid"
"github.com/hibiken/asynq"
"github.com/rs/zerolog"
+ "github.com/skip2/go-qrcode"
"github.com/swamphacks/core/apps/api/internal/config"
"github.com/swamphacks/core/apps/api/internal/email"
+ "github.com/swamphacks/core/apps/api/internal/storage"
"github.com/swamphacks/core/apps/api/internal/tasks"
)
type EmailService struct {
logger zerolog.Logger
- SESClient *email.SESClient
taskQueue *asynq.Client
+ SESClient *email.SESClient
+ storage storage.Storage
}
-func NewEmailService(taskQueue *asynq.Client, SESClient *email.SESClient, logger zerolog.Logger) *EmailService {
+func NewEmailService(taskQueue *asynq.Client, SESClient *email.SESClient, storage storage.Storage, logger zerolog.Logger) *EmailService {
return &EmailService{
logger: logger.With().Str("service", "EmailService").Str("component", "email").Logger(),
taskQueue: taskQueue,
SESClient: SESClient,
+ storage: storage,
}
}
@@ -31,7 +38,10 @@ func (s *EmailService) QueueConfirmationEmail(recipient string, name string) err
subject := "SwampHacks XI: we received your application!"
templateEmailFilepath := cfg.EmailTemplateDirectory + "ConfirmationEmail.html"
- _, err := s.QueueSendHtmlEmailTask(recipient, subject, name, templateEmailFilepath)
+ type emailTemplateData struct {
+ Name string
+ }
+ _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath)
if err != nil {
s.logger.Err(err).Msg("Failed to send confirmation email to recipient")
@@ -41,13 +51,56 @@ func (s *EmailService) QueueConfirmationEmail(recipient string, name string) err
return nil
}
+func (s *EmailService) QueueWelcomeEmail(ctx context.Context, recipient string, name string, userId uuid.UUID) error {
+ cfg := config.Load()
+
+ qrString := fmt.Sprintf("IDENT::%s", userId)
+ qrPng, err := qrcode.Encode(qrString, qrcode.Medium, 256)
+ if err != nil {
+ s.logger.Err(err).Msg("Failed to generate QR code png")
+ return err
+ }
+
+ contentType := "image/png"
+ if s.storage == nil {
+ s.logger.Err(err).Msg("A R2 client must be connected for this function to run")
+ return err
+ }
+ err = s.storage.Store(ctx, cfg.CoreBuckets.QRCodes, userId.String(), qrPng, &contentType)
+ if err != nil {
+ s.logger.Err(err).Msg("Failed to upload QR code to R2")
+ return err
+ }
+
+ qrPngLink := fmt.Sprintf("%s/%s", cfg.CoreBuckets.QRCodesBaseUrl, userId.String())
+
+ subject := "SwampHacks XI – A welcome from our Organizers!"
+ templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html"
+
+ type emailTemplateData struct {
+ Name string
+ QRPngLink string
+ }
+ _, err = s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name, QRPngLink: qrPngLink}, templateEmailFilepath)
+
+ if err != nil {
+ s.logger.Err(err).Msgf("Failed to send welcome email to recipient with userId %s", userId.String())
+ return err
+ }
+
+ return nil
+}
+
func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name string) error {
cfg := config.Load()
subject := "Congratulations! You're in – confirm in 72 hours to keep your spot in SwampHacks XI"
templateEmailFilepath := cfg.EmailTemplateDirectory + "WaitlistAcceptanceEmail.html"
- _, err := s.QueueSendHtmlEmailTask(recipient, subject, name, templateEmailFilepath)
+ type emailTemplateData struct {
+ Name string
+ }
+ _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath)
if err != nil {
s.logger.Err(err).Msg("Failed to send waitlist acceptance email to recipient")
@@ -57,7 +110,14 @@ func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name strin
return nil
}
-func (s *EmailService) SendHtmlEmail(recipient string, subject string, name string, templateFilePath string) error {
+// SendHtmlEmail
+//
+// templateData: a struct holding the data which should replace {{}} tags inside of an html template.
+// For example, if an email template uses the tag {{ .Name }}, then the templateData struct would look like
+// type templateData struct {
+// Name string
+// }
+func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateData interface{}, templateFilePath string) error {
var body bytes.Buffer
template, err := template.ParseFiles(templateFilePath)
@@ -65,7 +125,7 @@ func (s *EmailService) SendHtmlEmail(recipient string, subject string, name stri
s.logger.Err(err).Msg("Failed to parse email template for recipient")
}
- err = template.Execute(&body, struct{ Name string }{Name: name})
+ err = template.Execute(&body, templateData)
if err != nil {
s.logger.Err(err).Msg("Failed to inject template variables for recipient '%s'.")
}
@@ -80,19 +140,15 @@ func (s *EmailService) SendHtmlEmail(recipient string, subject string, name stri
return nil
}
-// TODO: refactor other queue functions to use a similar naming scheme
-func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, name string, templateFilePath string) (*asynq.TaskInfo, error) {
+func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, templateData interface{}, templateFilePath string) (*asynq.TaskInfo, error) {
if len(to) == 0 {
s.logger.Warn().Msgf("No recipient email found for email being sent from template '%s'", templateFilePath)
}
- if len(name) == 0 {
- s.logger.Warn().Msgf("No recipient name found for email being sent from template '%s'", templateFilePath)
- }
task, err := tasks.NewTaskSendHtmlEmail(tasks.SendHtmlEmailPayload{
To: to,
Subject: subject,
- Name: name,
+ TemplateData: templateData,
TemplateFilePath: templateFilePath,
})
diff --git a/apps/api/internal/tasks/email.go b/apps/api/internal/tasks/email.go
index 08acda63..1de132c7 100644
--- a/apps/api/internal/tasks/email.go
+++ b/apps/api/internal/tasks/email.go
@@ -19,8 +19,8 @@ type SendTextEmailPayload struct {
type SendHtmlEmailPayload struct {
To string
- Name string
Subject string
+ TemplateData interface{}
TemplateFilePath string
}
diff --git a/apps/api/internal/workers/email.go b/apps/api/internal/workers/email.go
index a881de80..77a6478f 100644
--- a/apps/api/internal/workers/email.go
+++ b/apps/api/internal/workers/email.go
@@ -30,7 +30,7 @@ func (w *EmailWorker) HandleSendHtmlEmailTask(ctx context.Context, t *asynq.Task
return fmt.Errorf("HandleSendHtmlEmailTask: json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
- if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.Name, p.TemplateFilePath); err != nil {
+ if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.TemplateData, p.TemplateFilePath); err != nil {
w.logger.Err(err).Msg("Failed to send ConfirmationEmail from worker")
return err
}