package index import ( "fmt" "html/template" "net/http" "strconv" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/mail" "code.nonshy.com/nonshy/website/pkg/markdown" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/ratelimit" "code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/templates" ) // Contact or report a problem. func Contact() http.HandlerFunc { tmpl := templates.Must("contact.html") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Query and form POST parameters. var ( intent = r.FormValue("intent") subject = r.FormValue("subject") title = "Contact Us" message = r.FormValue("message") footer string // appends to the message only when posting the feedback replyTo = r.FormValue("email") trap1 = r.FormValue("url") != "https://" trap2 = r.FormValue("comment") != "" tableID int tableName string tableLabel string // front-end user feedback about selected report item aboutUser *models.User // associated user (e.g. owner of reported photo) messageRequired = true // unless we have a table ID to work with success = "Thank you for your feedback! Your message has been delivered to the website administrators." ) // For report intents: ID of the user, photo, message, etc. tableID, err := strconv.Atoi(r.FormValue("id")) if err != nil { // The tableID is not an int - was it a username? if user, err := models.FindUser(r.FormValue("id")); err == nil { tableID = int(user.ID) } } if tableID > 0 { messageRequired = false } // In what context is the ID given? if subject != "" && tableID > 0 { switch subject { case "report.user": tableName = "users" if user, err := models.GetUser(uint64(tableID)); err == nil { tableLabel = fmt.Sprintf(`User account "%s"`, user.Username) aboutUser = user } else { log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err) } case "report.photo": tableName = "photos" // Find this photo and the user associated. if pic, err := models.GetPhoto(uint64(tableID)); err == nil { if user, err := models.GetUser(pic.UserID); err == nil { tableLabel = fmt.Sprintf(`A profile photo of user account "%s"`, user.Username) aboutUser = user } else { log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err) } } else { log.Error("/contact: couldn't produce table label for photo %d: %s", tableID, err) } case "report.message": tableName = "messages" tableLabel = "Direct Message conversation" // Find this message, and attach it to the report. if msg, err := models.GetMessage(uint64(tableID)); err == nil { var username = "[unavailable]" if sender, err := models.GetUser(msg.SourceUserID); err == nil { username = sender.Username aboutUser = sender } footer = fmt.Sprintf(` --- From: @%s %s`, username, username, markdown.Quotify(msg.Message), ) } case "report.comment": tableName = "comments" // Find this comment. if comment, err := models.GetComment(uint64(tableID)); err == nil { tableLabel = fmt.Sprintf(`A comment written by "%s"`, comment.User.Username) aboutUser = &comment.User } else { log.Error("/contact: couldn't produce table label for comment %d: %s", tableID, err) } case "report.forum", "forum.adopt": tableName = "forums" // Find this forum. if forum, err := models.GetForum(uint64(tableID)); err == nil { tableLabel = fmt.Sprintf(`The forum "%s" (/f/%s)`, forum.Title, forum.Fragment) } else { log.Error("/contact: couldn't produce table label for comment %d: %s", tableID, err) } } } // On POST: take what we have now and email the admins. if r.Method == http.MethodPost { // Look up the current user, in case logged in. currentUser, err := session.CurrentUser(r) if err == nil { replyTo = currentUser.Email } // We were getting too much spam logged-out: prevent logged-out bots from still posting. if currentUser == nil { log.Error("Blocked POST /contact because user is logged-out") session.FlashError(w, r, "Our contact form is only for logged-in users, sorry!") templates.Redirect(w, "/contact") return } // Rate limit submissions, especially for logged-out users. if currentUser == nil { limiter := &ratelimit.Limiter{ Namespace: "contact", ID: session.RemoteAddr(r), Limit: config.ContactRateLimit, Window: config.ContactRateLimitWindow, CooldownAt: config.ContactRateLimitCooldownAt, Cooldown: config.ContactRateLimitCooldown, } if err := limiter.Ping(); err != nil { session.FlashError(w, r, err.Error()) templates.Redirect(w, r.URL.Path) return } } // If they have tripped the spam bot trap fields, don't save their message. if trap1 || trap2 { log.Error("Contact form: bot has tripped the trap fields, do not save message") session.Flash(w, r, success) templates.Redirect(w, r.URL.Path) return } // Store feedback in the database. fb := &models.Feedback{ Intent: intent, Subject: subject, Message: message + footer, TableName: tableName, TableID: uint64(tableID), } if aboutUser != nil { fb.AboutUserID = aboutUser.ID } if currentUser != nil && currentUser.ID > 0 { fb.UserID = currentUser.ID } else if replyTo != "" { fb.ReplyTo = replyTo } if err := models.CreateFeedback(fb); err != nil { session.FlashError(w, r, "Couldn't save feedback: %s", err) templates.Redirect(w, r.URL.Path) return } // Email the admins. if err := mail.Send(mail.Message{ To: config.Current.AdminEmail, Subject: "User Feedback: " + title, Template: "email/contact_admin.html", Data: map[string]interface{}{ "Title": title, "Intent": intent, "Subject": subject, "Message": template.HTML(markdown.Render(message)), "TableName": tableName, "TableID": tableID, "CurrentUser": currentUser, "ReplyTo": replyTo, "BaseURL": config.Current.BaseURL, "AdminURL": config.Current.BaseURL + "/admin/feedback", }, }); err != nil { log.Error("/contact page: couldn't send email: %s", err) } session.Flash(w, r, success) templates.Redirect(w, r.URL.Path) return } // Default intent = contact if intent == "report" { title = "Report a Problem" } else { intent = "contact" } // Validate the subject. if subject != "" { var found bool for _, group := range config.ContactUsChoices { for _, opt := range group.Options { if opt.Value == subject { found = true break } } } if !found { subject = "" } } var vars = map[string]interface{}{ "Intent": intent, "TableID": r.FormValue("id"), "TableLabel": tableLabel, "Subject": subject, "PageTitle": title, "Subjects": config.ContactUsChoices, "Message": message, "MessageRequired": messageRequired, } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) }