Skip to content

Commit

Permalink
added a feedback cronjob
Browse files Browse the repository at this point in the history
  • Loading branch information
CommanderStorm committed Sep 23, 2023
1 parent 9ec2b92 commit dcbeaf2
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ APNS_P8_FILE_PATH=/secrets/AuthKey_XXXX.p8
SENTRY_DSN=

CAMPUS_API_TOKEN=

SMTP_PASSWORD=
SMTP_URL=
SMTP_USERNAME=
SMTP_PORT=
4 changes: 4 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ services:
- MensaCronDisabled=true
- OMDB_API_KEY=${OMDB_API_KEY}
- CAMPUS_API_TOKEN=${CAMPUS_API_TOKEN}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_URL=${SMTP_URL:-postout.lrz.de}
- SMTP_USERNAME=${SMTP_USERNAME:[email protected]}
- SMTP_PORT=${SMTP_PORT:-587}
volumes:
- backend-storage:/Storage
- ./apns_auth_key.p8:${APNS_P8_FILE_PATH}
Expand Down
6 changes: 5 additions & 1 deletion server/backend/cron/cronjobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
IOSActivityReset = "iosActivityReset"
NewExamResultsHook = "newExamResultsHook"
MovieType = "movie"
FeedbackEmail = "feedbackEmail"

/* MensaType = "mensa"
AlarmType = "alarm" */
Expand Down Expand Up @@ -59,7 +60,7 @@ func (c *CronService) Run() error {
var res []model.Crontab

c.db.Model(&model.Crontab{}).
Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?)",
Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
time.Now().Unix(),
NewsType,
FileDownloadType,
Expand All @@ -70,6 +71,7 @@ func (c *CronService) Run() error {
IOSActivityReset,
NewExamResultsHook,
MovieType,
FeedbackEmail,
).
Scan(&res)

Expand Down Expand Up @@ -125,6 +127,8 @@ func (c *CronService) Run() error {
g.Go(func() error { return c.iosNotificationsCron() })
case IOSActivityReset:
g.Go(func() error { return c.iosActivityReset() })
case FeedbackEmail:
g.Go(func() error { return c.feedbackEmailCron() })
}
}

Expand Down
42 changes: 42 additions & 0 deletions server/backend/cron/emailTemplates/feedbackBody.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<h1>Feedback via TumCampusApp:</h1>
{{ if .Feedback.Valid -}}
<blockquote>
{{- .Feedback.String -}}
</blockquote>
{{- else -}}
<i>no feedback provided</i>
{{- end }}
<table>
<tr>
<th>Inforation type</th>
<th>Details</th>
</tr>
{{- if .Latitude.Valid }}
<tr>
<th>Nutzer-Standort</th>
<td>
<a href="https://www.google.com/maps/search/?api=1&query={{ .Latitude.Float64 }},{{ .Longitude.Float64 }}">
latitude: {{ .Latitude.Float64 }}, longitude: {{ .Longitude.Float64 }}
</a>
</td>
</tr>
{{- end }}
<tr>
<th>OS-Version</th>
<td>{{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}}</td>
</tr>
<tr>
<th>App-Version</th>
<td>{{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}}</td>
</tr>
</table>
{{- if .ImageCount }}
<h2>Fotos:</h2><br/>
<ol>
{{- range $val := iterate .ImageCount }}
<li>
<a href="https://app.tum.de/File/feedback/{{ $.Id }}/{{ $val }}.png">Foto {{ $val }}</a>
</li>
{{- end }}
</ol>
{{- end -}}
22 changes: 22 additions & 0 deletions server/backend/cron/emailTemplates/feedbackBody.txt.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Feedback via TumCampusApp:

{{ if .Feedback.Valid }}
{{- .Feedback.String -}}
{{ else -}}
no feedback provided
{{- end }}

Metadata:
{{- if .Latitude.Valid }}
- Nutzer-Standort: {{ .Latitude.Float64 }},{{ .Longitude.Float64 }} (latitude,longitude)
https://www.google.com/maps/search/?api=1&query={{ .Latitude.Float64 }},{{ .Longitude.Float64 }}
{{- end }}
- OS-Version: {{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}}
- App-Version: {{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}}
{{- if .ImageCount }}

Photos:
{{- range $val := iterate .ImageCount }}
- Photo {{ $val }}: https://app.tum.de/File/feedback/{{ $.Id }}/{{ $val }}.png
{{- end -}}
{{- end -}}
139 changes: 139 additions & 0 deletions server/backend/cron/feedbackEmail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package cron

import (
"bytes"
"crypto/tls"
htmlTemplate "html/template"
"os"
"strconv"
textTemplate "text/template"
"time"

"github.com/TUM-Dev/Campus-Backend/server/model"
log "github.com/sirupsen/logrus"
"gopkg.in/gomail.v2"

_ "embed"
)

// iterate is necessary, as go otherwise cannot count up in a for loop inside templates
func iterate(count int32) []int32 {
var items []int32
var i int32
for i = 0; i < count; i++ {
items = append(items, i)
}
return items
}

//go:embed emailTemplates/feedbackBody.gohtml
var htmlFeedbackBody string

//go:embed emailTemplates/feedbackBody.txt.tmpl
var txtFeedbackBody string

func parseTemplates() (*htmlTemplate.Template, *textTemplate.Template, error) {
funcMap := textTemplate.FuncMap{"iterate": iterate}
parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody)
if err != nil {
return nil, nil, err
}
parsedTxtBody, err := textTemplate.New("txtFeedbackBody").Funcs(funcMap).Parse(txtFeedbackBody)
if err != nil {
return nil, nil, err
}
return parsedHtmlBody, parsedTxtBody, nil

}

type MailHeaders struct {
From string
To string
ReplyTo string //optional
Timestamp time.Time
Subject string
}

func messageWithHeaders(feedback *model.Feedback) *gomail.Message {
m := gomail.NewMessage()
// From
m.SetAddressHeader("From", os.Getenv("SMTP_USERNAME"), "TUM Campus App")
// To
if feedback.Receiver.Valid {
m.SetHeader("To", feedback.Receiver.String)
} else {
m.SetHeader("To", "[email protected]")
}
// ReplyTo
if feedback.ReplyTo.Valid {
m.SetHeader("Reply-To", feedback.ReplyTo.String)
}
// Timestamp
if feedback.Timestamp.Valid {
m.SetDateHeader("Date", feedback.Timestamp.Time)
} else {
m.SetDateHeader("Date", time.Now())
}
// Subject
m.SetHeader("Subject", "Feedback via Tum Campus App")
return m
}

func generateTemplatedMail(parsedHtmlBody *htmlTemplate.Template, parsedTxtBody *textTemplate.Template, feedback *model.Feedback) (string, string, error) {
var htmlBodyBuffer bytes.Buffer
if err := parsedHtmlBody.Execute(&htmlBodyBuffer, feedback); err != nil {
return "", "", err
}
var txtBodyBuffer bytes.Buffer
if err := parsedTxtBody.Execute(&txtBodyBuffer, feedback); err != nil {
return "", "", err
}
return htmlBodyBuffer.String(), txtBodyBuffer.String(), nil
}

func (c *CronService) feedbackEmailCron() error {

var results []model.Feedback
if err := c.db.Find(&results, "processed = false").Scan(&results).Error; err != nil {
log.WithError(err).Fatal("could not get unprocessed feedback")
return err
}
parsedHtmlBody, parsedTxtBody, err := parseTemplates()
if err != nil {
log.WithError(err).Fatal("could not parse email templates")
return err
}

smtpPort, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
if err != nil {
log.WithError(err).Fatal("SMTP_PORT is not an integer")
return err
}
d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"))
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
for i, feedback := range results {
m := messageWithHeaders(&feedback)

// attach a body
htmlBodyBuffer, txtBodyBuffer, err := generateTemplatedMail(parsedHtmlBody, parsedTxtBody, &feedback)
if err != nil {
log.WithError(err).Error("Could not template mail body")
return err
}
m.SetBody("text/plain", txtBodyBuffer)
m.AddAlternative("text/html", htmlBodyBuffer)

// send mail
if err := d.DialAndSend(m); err != nil {
log.WithError(err).Error("could not send mail")
continue
}
log.Tracef("sending feedback %d to %v successfull", i, feedback.Receiver)

// prevent the message being send the next time around
if err := c.db.Find(model.Feedback{}, "id = ?", feedback.Id).Update("processed", "true").Error; err != nil {
log.WithError(err).Error("could not prevent mail from being send again")
}
}
return nil
}
66 changes: 66 additions & 0 deletions server/backend/migration/20230826000000.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package migration

import (
"database/sql"

"github.com/TUM-Dev/Campus-Backend/server/model"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/guregu/null"
"gorm.io/gorm"
)

type Feedback struct {
Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"`
OsVersion sql.NullString `gorm:"column:os_version;type:text;null;"`
AppVersion sql.NullString `gorm:"column:app_version;type:text;null;"`
}

// TableName sets the insert table name for this struct type
func (n *Feedback) TableName() string {
return "feedback"
}

// migrate20230826000000
// adds a "feedbackEmail" cron job that runs every 30 minutes.
func (m TumDBMigrator) migrate20230826000000() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "20230826000000",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().AddColumn(&Feedback{}, "Processed"); err != nil {
return err
}
if err := tx.Migrator().AddColumn(&Feedback{}, "OsVersion"); err != nil {
return err
}
if err := tx.Migrator().AddColumn(&Feedback{}, "AppVersion"); err != nil {
return err
}
if err := tx.Exec("UPDATE feedback SET processed = true WHERE processed != true;").Error; err != nil {
return err
}
if err := SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail"); err != nil {
return err
}
return tx.Create(&model.Crontab{
Interval: 60 * 30, // Every 30 minutes
Type: null.String{NullString: sql.NullString{String: "feedbackEmail", Valid: true}},
}).Error
},

Rollback: func(tx *gorm.DB) error {
if err := tx.Migrator().DropColumn(&Feedback{}, "Processed"); err != nil {
return err
}
if err := tx.Migrator().DropColumn(&Feedback{}, "OsVersion"); err != nil {
return err
}
if err := tx.Migrator().DropColumn(&Feedback{}, "AppVersion"); err != nil {
return err
}
if err := tx.Delete(&model.Crontab{}, "type = ? AND interval = ?", "fileDownload", 30*60).Error; err != nil {
return err
}
return SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail")
},
}
}
1 change: 1 addition & 0 deletions server/backend/migration/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func (m TumDBMigrator) Migrate() error {
m.migrate20230904000000(),
m.migrate20230530000000(),
m.migrate20230904100000(),
m.migrate20230826000000(),
})
err := mig.Migrate()
return err
Expand Down
2 changes: 2 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb
google.golang.org/grpc v1.58.0
google.golang.org/protobuf v1.31.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/mysql v1.5.1
gorm.io/gorm v1.25.4
)
Expand Down Expand Up @@ -55,5 +56,6 @@ require (
golang.org/x/text v0.13.0 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand All @@ -910,6 +912,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
Expand Down
25 changes: 25 additions & 0 deletions server/model/feedback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package model

import (
"github.com/guregu/null"
)

type Feedback struct {
Id int64 `gorm:"column:id;primary_key;AUTO_INCREMENT;type:int;not null;"`
ImageCount int32 `gorm:"column:image_count;type:int;not null;"`
EmailId null.String `gorm:"column:email_id;type:text;null"`
Receiver null.String `gorm:"column:receiver;type:text;null"`
ReplyTo null.String `gorm:"column:reply_to;type:text;null"`
Feedback null.String `gorm:"column:feedback;type:text;null"`
Latitude null.Float `gorm:"column:latitude;type:float;null;"`
Longitude null.Float `gorm:"column:longitude;type:float;null;"`
OsVersion null.String `gorm:"column:os_version;type:text;null;"`
AppVersion null.String `gorm:"column:app_version;type:text;null;"`
Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"`
Timestamp null.Time `gorm:"column:timestamp;type:timestamp;default:CURRENT_TIMESTAMP;null;"`
}

// TableName sets the insert table name for this struct type
func (n *Feedback) TableName() string {
return "feedback"
}

0 comments on commit dcbeaf2

Please sign in to comment.