package server import ( "encoding/base64" "net/http" "github.com/gin-gonic/gin" ) // feedbackSubmitRequest is the player's feedback submission. Attachment is the // base64-encoded file bytes (empty for none); AttachmentName carries the original // file name (its extension is the allow-list key); Channel is the submitting // platform (telegram/ios/android/web). type feedbackSubmitRequest struct { Body string `json:"body"` Attachment string `json:"attachment"` AttachmentName string `json:"attachment_name"` Channel string `json:"channel"` } // feedbackReplyDTO is the operator's reply shown back to the player. type feedbackReplyDTO struct { Body string `json:"body"` RepliedAtUnix int64 `json:"replied_at_unix"` } // feedbackStateResponse is the player's feedback screen state. BlockedReason is // "" (can send), "pending" or "banned"; Reply is omitted when there is none. type feedbackStateResponse struct { CanSend bool `json:"can_send"` BlockedReason string `json:"blocked_reason"` Reply *feedbackReplyDTO `json:"reply,omitempty"` } // feedbackUnreadResponse reports whether the player has an undelivered reply, for // the lobby/Info badge. type feedbackUnreadResponse struct { ReplyUnread bool `json:"reply_unread"` } // handleFeedbackSubmit stores a feedback message from the authenticated player. // The sender IP comes from the gateway-forwarded X-Forwarded-For header. Guests // and feedback-banned accounts are refused (also gated at the gateway). func (s *Server) handleFeedbackSubmit(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } var req feedbackSubmitRequest if err := c.ShouldBindJSON(&req); err != nil { abortBadRequest(c, "invalid request body") return } var attachment []byte if req.Attachment != "" { data, err := base64.StdEncoding.DecodeString(req.Attachment) if err != nil { abortBadRequest(c, "invalid attachment encoding") return } attachment = data } if err := s.feedback.Submit(c.Request.Context(), uid, req.Body, attachment, req.AttachmentName, req.Channel, clientIP(c)); err != nil { s.abortErr(c, err) return } c.JSON(http.StatusOK, okResponse{OK: true}) } // handleFeedbackState returns the player's feedback screen state and marks any // pending operator replies delivered (clearing the badge). func (s *Server) handleFeedbackState(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } st, err := s.feedback.State(c.Request.Context(), uid) if err != nil { s.abortErr(c, err) return } resp := feedbackStateResponse{CanSend: st.CanSend, BlockedReason: st.BlockedReason} if st.Reply != nil { resp.Reply = &feedbackReplyDTO{Body: st.Reply.Body, RepliedAtUnix: st.Reply.RepliedAt.Unix()} } c.JSON(http.StatusOK, resp) } // handleFeedbackUnread reports whether the player has an undelivered operator // reply, for the lobby/Info badge. It has no side effect. func (s *Server) handleFeedbackUnread(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } unread, err := s.feedback.ReplyUnread(c.Request.Context(), uid) if err != nil { s.abortErr(c, err) return } c.JSON(http.StatusOK, feedbackUnreadResponse{ReplyUnread: unread}) }