Skip to content

Commit

Permalink
implement email notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobmichels committed Sep 7, 2022
1 parent f293631 commit 94b3092
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 20 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cloud.google.com/go/firestore v1.6.1
github.com/julienschmidt/httprouter v1.3.0
github.com/spf13/viper v1.12.0
github.com/twilio/twilio-go v0.26.0
google.golang.org/api v0.81.0
)

Expand All @@ -14,6 +15,7 @@ require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
Expand All @@ -22,6 +24,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand Down
12 changes: 11 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -97,6 +98,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand All @@ -111,6 +113,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -196,14 +199,18 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -233,6 +240,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
github.com/twilio/twilio-go v0.26.0 h1:wFW4oTe3/LKt6bvByP7eio8JsjtaLHjMQKOUEzQry7U=
github.com/twilio/twilio-go v0.26.0/go.mod h1:lz62Hopu4vicpQ056H5TJ0JE4AP0rS3sQ35/ejmgOwE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -674,8 +683,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
Expand Down
41 changes: 41 additions & 0 deletions internal/notifier/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package notifier

import (
"context"
"fmt"
"log"
"net/smtp"

"github.com/jacobmichels/Course-Sense-Go/internal/types"
)

type EmailNotifier struct {
host string
port int
username string
password string
from string
}

func NewEmailNotifier(host, username, password, from string, port int) *EmailNotifier {
return &EmailNotifier{host, port, username, password, from}
}

func (en *EmailNotifier) Name() string {
return "EmailNotifier"
}

func (en *EmailNotifier) Notify(ctx context.Context, section *types.CourseSection) error {
auth := smtp.PlainAuth("", en.username, en.password, en.host)
msg := []byte(fmt.Sprintf("Hello from Course Sense!\n\nSpace has been found in the following course section: %s %d %s %s. Get over to WebAdvisor to claim the spot!", section.Department, section.CourseCode, section.SectionCode, section.Term))

for _, watcher := range section.Watchers {
err := smtp.SendMail(fmt.Sprintf("%s:%d", en.host, en.port), auth, en.from, []string{watcher.Email}, msg)
if err != nil {
return fmt.Errorf("failed to notify %s: %w", watcher.Email, err)
}
log.Println("mail sent")
}

return nil
}
11 changes: 0 additions & 11 deletions internal/notifier/notifier.go

This file was deleted.

32 changes: 32 additions & 0 deletions internal/notifier/twilio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package notifier

import (
"context"

"github.com/jacobmichels/Course-Sense-Go/internal/types"
"github.com/twilio/twilio-go"
)

type TwilioNotifier struct {
accountSid string
authToken string
phoneNumber string
client *twilio.RestClient
}

func NewTwilioNotifier(accountSid, authToken, phoneNumber string) *TwilioNotifier {
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})

return &TwilioNotifier{accountSid, authToken, phoneNumber, client}
}

func (tn *TwilioNotifier) Name() string {
return "TwilioNotifier"
}

func (tn *TwilioNotifier) Notify(ctx context.Context, section *types.CourseSection) error {
return nil
}
12 changes: 12 additions & 0 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ type AppConfig struct {
CredentialsFilePath string `mapstructure:"credentials_file"`
CollectionID string `mapstructure:"collection_id"`
}
Twilio struct {
AccountSID string `mapstructure:"account_sid"`
AuthToken string `mapstructure:"auth_token"`
PhoneNumber string `mapstructure:"phone_number"`
}
Smtp struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
From string `mapstructure:"from"`
}
}

func readAppConfig() (*AppConfig, error) {
Expand Down
15 changes: 8 additions & 7 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"

"github.com/jacobmichels/Course-Sense-Go/internal/database"
"github.com/jacobmichels/Course-Sense-Go/internal/notifier"
"github.com/jacobmichels/Course-Sense-Go/internal/types"
"github.com/jacobmichels/Course-Sense-Go/internal/webadvisor"
"github.com/julienschmidt/httprouter"
Expand All @@ -25,11 +24,11 @@ func pingHandler() func(http.ResponseWriter, *http.Request, httprouter.Params) {
}
}

func triggerHandler(db *database.FirestoreDatabase, wa *webadvisor.WebAdvisor) func(http.ResponseWriter, *http.Request, httprouter.Params) {
func triggerHandler(db *database.FirestoreDatabase, wa *webadvisor.WebAdvisor, notifiers ...Notifier) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
log.Println("Trigger request receieved")

if err := trigger(r.Context(), db, wa); err != nil {
if err := trigger(r.Context(), db, wa, notifiers...); err != nil {
log.Printf("Error occured during slot check: %s", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand All @@ -38,7 +37,7 @@ func triggerHandler(db *database.FirestoreDatabase, wa *webadvisor.WebAdvisor) f
}
}

func trigger(ctx context.Context, db *database.FirestoreDatabase, wa *webadvisor.WebAdvisor) error {
func trigger(ctx context.Context, db *database.FirestoreDatabase, wa *webadvisor.WebAdvisor, notifiers ...Notifier) error {
sections, ids, err := db.GetSections(ctx)
if err != nil {
return fmt.Errorf("failed to list sections")
Expand All @@ -58,9 +57,11 @@ func trigger(ctx context.Context, db *database.FirestoreDatabase, wa *webadvisor
if capacity > 0 {
log.Printf("Section %s %d %s %s has %d capacity\n", section.Department, section.CourseCode, section.SectionCode, section.Term, capacity)

err := notifier.NotifyWatchers(ctx, section)
if err != nil {
log.Printf("failed to notify watchers: %s", err)
for _, notifier := range notifiers {
err = notifier.Notify(ctx, section)
if err != nil {
log.Printf("%s failed to notify: %s", notifier.Name(), err)
}
}

sectionID := ids[i]
Expand Down
11 changes: 10 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import (
"time"

"github.com/jacobmichels/Course-Sense-Go/internal/database"
"github.com/jacobmichels/Course-Sense-Go/internal/notifier"
"github.com/jacobmichels/Course-Sense-Go/internal/types"
"github.com/jacobmichels/Course-Sense-Go/internal/webadvisor"
"github.com/julienschmidt/httprouter"
)

type Notifier interface {
Notify(context.Context, *types.CourseSection) error
Name() string
}

func Run(ctx context.Context) error {
config, err := readAppConfig()
if err != nil {
Expand All @@ -27,14 +34,16 @@ func Run(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to create webadvisor service: %w", err)
}
tn := notifier.NewTwilioNotifier(config.Twilio.AccountSID, config.Twilio.AuthToken, config.Twilio.PhoneNumber)
en := notifier.NewEmailNotifier(config.Smtp.Host, config.Smtp.Username, config.Smtp.Password, config.Smtp.From, config.Smtp.Port)

r := httprouter.New()

// Ping endpoint for health check
r.GET("/ping", pingHandler())
// Triggers the empty space check
// This endpoint is meant to be called by a cron job
r.GET("/trigger", triggerHandler(db, wa))
r.GET("/trigger", triggerHandler(db, wa, tn, en))
// Registers notifications
r.PUT("/register", registerHandler(db, wa))

Expand Down

0 comments on commit 94b3092

Please sign in to comment.