diff --git a/.env b/.env
index e8387199..132f9c97 100644
--- a/.env
+++ b/.env
@@ -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=
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 72154cde..9bc5cc4a 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -18,6 +18,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:-tca-support.os.in@tum.de}
+ - SMTP_PORT=${SMTP_PORT:-587}
volumes:
- backend-storage:/Storage
- ./apns_auth_key.p8:${APNS_P8_FILE_PATH}
diff --git a/server/backend/cron/cronjobs.go b/server/backend/cron/cronjobs.go
index 396ca328..55c8bce0 100644
--- a/server/backend/cron/cronjobs.go
+++ b/server/backend/cron/cronjobs.go
@@ -32,6 +32,7 @@ const (
IOSActivityReset = "iosActivityReset"
NewExamResultsHook = "newExamResultsHook"
MovieType = "movie"
+ FeedbackEmail = "feedbackEmail"
/* MensaType = "mensa"
AlarmType = "alarm" */
@@ -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,
@@ -70,6 +71,7 @@ func (c *CronService) Run() error {
IOSActivityReset,
NewExamResultsHook,
MovieType,
+ FeedbackEmail,
).
Scan(&res)
@@ -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() })
}
}
diff --git a/server/backend/cron/email_templates/feedback_body.gohtml b/server/backend/cron/email_templates/feedback_body.gohtml
new file mode 100644
index 00000000..411eb53e
--- /dev/null
+++ b/server/backend/cron/email_templates/feedback_body.gohtml
@@ -0,0 +1,42 @@
+
Feedback via TumCampusApp:
+{{ if .Feedback.Valid -}}
+
+ {{- .Feedback.String -}}
+
+{{- else -}}
+no feedback provided
+{{- end }}
+
+{{- if .ImageCount }}
+Fotos:
+
+{{- range $val := iterate .ImageCount }}
+ -
+ Foto {{ $val }}
+
+{{- end }}
+
+{{- end -}}
diff --git a/server/backend/cron/email_templates/feedback_body.txt.tmpl b/server/backend/cron/email_templates/feedback_body.txt.tmpl
new file mode 100644
index 00000000..e4c6c7d0
--- /dev/null
+++ b/server/backend/cron/email_templates/feedback_body.txt.tmpl
@@ -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 -}}
diff --git a/server/backend/cron/feedback_email.go b/server/backend/cron/feedback_email.go
new file mode 100644
index 00000000..01d39867
--- /dev/null
+++ b/server/backend/cron/feedback_email.go
@@ -0,0 +1,146 @@
+package cron
+
+import (
+ "bytes"
+ 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"
+)
+
+//go:embed email_templates/feedback_body.gohtml
+var htmlFeedbackBody string
+
+//go:embed email_templates/feedback_body.txt.tmpl
+var txtFeedbackBody string
+
+// iterate is a template helper to make counting possible
+func iterate(count int32) []int32 {
+ var items []int32
+ var i int32
+ for i = 0; i < count; i++ {
+ items = append(items, i)
+ }
+ return items
+}
+
+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", "app@tum.de")
+ }
+ // 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
+ }
+
+ dialer, err := setupSMTPDialer()
+ if err != nil {
+ return err
+ }
+ 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 := dialer.DialAndSend(m); err != nil {
+ log.WithError(err).Error("could not send mail")
+ continue
+ }
+ log.Tracef("sending feedback %dialer 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
+}
+
+// setupSMTPDialer sets up the SMTP dialer
+func setupSMTPDialer() (*gomail.Dialer, error) {
+ smtpPort, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
+ if err != nil {
+ log.WithError(err).Fatal("SMTP_PORT is not an integer")
+ return nil, err
+ }
+ d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"))
+ return d, nil
+}
diff --git a/server/backend/cron/feedback_email_test.go b/server/backend/cron/feedback_email_test.go
new file mode 100644
index 00000000..7c5f1a2e
--- /dev/null
+++ b/server/backend/cron/feedback_email_test.go
@@ -0,0 +1,157 @@
+package cron
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/guregu/null"
+
+ "github.com/TUM-Dev/Campus-Backend/server/model"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestIterate(t *testing.T) {
+ assert.Equal(t, []int32(nil), iterate(0))
+ assert.Equal(t, []int32{0}, iterate(1))
+ assert.Equal(t, []int32{0, 1}, iterate(2))
+ assert.Equal(t, []int32{0, 1, 2}, iterate(3))
+ assert.Equal(t, []int32{0, 1, 2, 3}, iterate(4))
+ assert.Equal(t, 42, len(iterate(42)))
+}
+
+func fullFeedback() *model.Feedback {
+ return &model.Feedback{
+ EmailId: null.StringFrom("magic-id"),
+ Receiver: null.StringFrom("tca"),
+ ReplyTo: null.StringFrom("test@example.de"),
+ Feedback: null.StringFrom("This is a Test"),
+ ImageCount: 1,
+ Latitude: null.FloatFrom(0),
+ Longitude: null.FloatFrom(0),
+ AppVersion: null.StringFrom("TCA 10.2"),
+ OsVersion: null.StringFrom("Android 10.0"),
+ Timestamp: null.TimeFrom(time.Now()),
+ }
+}
+
+func emptyFeedback() *model.Feedback {
+ return &model.Feedback{
+ EmailId: null.String{},
+ Receiver: null.String{},
+ ReplyTo: null.String{},
+ Feedback: null.String{},
+ ImageCount: 0,
+ Latitude: null.Float{},
+ Longitude: null.Float{},
+ AppVersion: null.String{},
+ OsVersion: null.String{},
+ Timestamp: null.Time{},
+ }
+}
+
+func TestHeaderInstantiationWithFullFeedback(t *testing.T) {
+ require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de"))
+ fb := fullFeedback()
+ m := messageWithHeaders(fb)
+ assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From"))
+ assert.Equal(t, []string{fb.Receiver.String}, m.GetHeader("To"))
+ assert.Equal(t, []string{"test@example.de"}, m.GetHeader("Reply-To"))
+ assert.Equal(t, []string{fb.Timestamp.Time.Format(time.RFC1123Z)}, m.GetHeader("Date"))
+ assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject"))
+}
+
+func TestHeaderInstantiationWithEmptyFeedback(t *testing.T) {
+ require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de"))
+ m := messageWithHeaders(emptyFeedback())
+ assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From"))
+ assert.Equal(t, []string{"app@tum.de"}, m.GetHeader("To"))
+ assert.Equal(t, []string(nil), m.GetHeader("Reply-To"))
+ // Date is set to now in messageWithHeaders => checking that this is actually now is a bit tricker
+ dates := m.GetHeader("Date")
+ assert.Equal(t, 1, len(dates))
+ date, err := time.Parse(time.RFC1123Z, dates[0])
+ require.NoError(t, err)
+ assert.WithinDuration(t, time.Now(), date, time.Minute)
+ assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject"))
+}
+
+func TestTemplatingResultsWithFullFeedback(t *testing.T) {
+ html, txt, err := parseTemplates()
+ require.NoError(t, err)
+ htmlBody, txtBody, err := generateTemplatedMail(html, txt, fullFeedback())
+ require.NoError(t, err)
+ assert.Equal(t, `Feedback via TumCampusApp:
+This is a Test
+
+
+ Inforation type |
+ Details |
+
+
+ Nutzer-Standort |
+
+
+ latitude: 0, longitude: 0
+
+ |
+
+
+ OS-Version |
+ Android 10.0 |
+
+
+ App-Version |
+ TCA 10.2 |
+
+
+Fotos:
+
+ -
+ Foto 0
+
+
`, htmlBody)
+ assert.Equal(t, `Feedback via TumCampusApp:
+
+This is a Test
+
+Metadata:
+- Nutzer-Standort: 0,0 (latitude,longitude)
+ https://www.google.com/maps/search/?api=1&query=0,0
+- OS-Version: Android 10.0
+- App-Version: TCA 10.2
+
+Photos:
+- Photo 0: https://app.tum.de/File/feedback/0/0.png`, txtBody)
+}
+
+func TestTemplatingResultsWithEmptyFeedback(t *testing.T) {
+ html, txt, err := parseTemplates()
+ require.NoError(t, err)
+ htmlBody, txtBody, err := generateTemplatedMail(html, txt, emptyFeedback())
+ require.NoError(t, err)
+ assert.Equal(t, `Feedback via TumCampusApp:
+no feedback provided
+
+
+ Inforation type |
+ Details |
+
+
+ OS-Version |
+ unknown |
+
+
+ App-Version |
+ unknown |
+
+
`, htmlBody)
+ assert.Equal(t, `Feedback via TumCampusApp:
+
+no feedback provided
+
+Metadata:
+- OS-Version: unknown
+- App-Version: unknown`, txtBody)
+}
diff --git a/server/backend/migration/20230826000000.go b/server/backend/migration/20230826000000.go
new file mode 100644
index 00000000..c3ec81de
--- /dev/null
+++ b/server/backend/migration/20230826000000.go
@@ -0,0 +1,64 @@
+package migration
+
+import (
+ "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 null.String `gorm:"column:os_version;type:text;null;"`
+ AppVersion null.String `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.StringFrom("feedbackEmail"),
+ }).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: null.StringFrom("fileDownload")}).Error; err != nil {
+ return err
+ }
+ return SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail")
+ },
+ }
+}
diff --git a/server/backend/migration/migration.go b/server/backend/migration/migration.go
index b5dcbcf5..d975f58c 100644
--- a/server/backend/migration/migration.go
+++ b/server/backend/migration/migration.go
@@ -52,6 +52,7 @@ func (m TumDBMigrator) Migrate() error {
m.migrate20230904000000(),
m.migrate20230530000000(),
m.migrate20230904100000(),
+ m.migrate20230826000000(),
})
err := mig.Migrate()
return err
diff --git a/server/go.mod b/server/go.mod
index 01399b80..b404eec6 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -26,6 +26,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb
google.golang.org/grpc v1.58.2
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
)
@@ -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
)
diff --git a/server/go.sum b/server/go.sum
index 9a6a4cf6..cf378dba 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -906,6 +906,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=
@@ -914,6 +916,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=
diff --git a/server/model/feedback.go b/server/model/feedback.go
new file mode 100644
index 00000000..6a0ea3d1
--- /dev/null
+++ b/server/model/feedback.go
@@ -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"
+}