package service import ( "encoding/json" "fmt" "gitea.pena/SQuiz/answerer/clients" quizdal "gitea.pena/SQuiz/answerer/dal" "gitea.pena/SQuiz/answerer/middleware" "gitea.pena/SQuiz/answerer/model" "github.com/gofiber/fiber/v2" "strconv" "strings" "time" "unicode/utf8" "github.com/rs/xid" ) const ( quizIdCookie = "qud" fingerprintCookie = "fp" ) type Service struct { dal *quizdal.DAL aiClient *clients.AIClient quizConfigData GetQuizDataResp } type ServiceDeps struct { Dal *quizdal.DAL AiClient *clients.AIClient } func New(deps ServiceDeps) (*Service, error) { quizData, err := loadQuizDataConfig() if err != nil { return nil, err } return &Service{ dal: deps.Dal, aiClient: deps.AiClient, quizConfigData: quizData, }, nil } func (s *Service) Register(app *fiber.App) *fiber.App { app.Post("/answer", s.PutAnswersOnePiece) app.Post("/settings", s.GetQuizData) return app } type GetQuizDataResp struct { Settings *ShavedQuiz `json:"settings,omitempty"` Items []ShavedQuestion `json:"items"` Count uint64 `json:"cnt"` ShowBadge bool `json:"show_badge"` } type ShavedQuiz struct { Fingerprinting bool `json:"fp"` Repeatable bool `json:"rep"` Name string `json:"name"` Config string `json:"cfg"` Limit uint64 `json:"lim"` DueTo uint64 `json:"due"` TimeOfPassing uint64 `json:"delay"` Pausable bool `json:"pausable"` Status string `json:"status"` } type ShavedQuestion struct { Id uint64 `json:"id"` Title string `json:"title"` Description string `json:"desc"` Type string `json:"typ"` Required bool `json:"req"` Page int `json:"p"` Content string `json:"c"` } // GetQuizData handler for obtaining data for quiz front rendering func (s *Service) GetQuizData(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(s.quizConfigData) } // MB Size constants const ( MB = 1 << 20 filePrefix = "file:" ) type PutAnswersResponse struct { FileIDMap map[uint64]string `json:"fileIDMap"` Stored []uint64 `json:"stored"` } func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error { cs, ok := c.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string) if !ok { return c.Status(fiber.StatusUnauthorized).SendString("no session in cookie") } form, err := c.MultipartForm() if err != nil || form == nil || form.File == nil { return c.Status(fiber.StatusBadRequest).SendString("expecting multipart form file") } answersStr := form.Value["answers"] if len(answersStr) == 0 { return c.Status(fiber.StatusFailedDependency).SendString("no answers provided") } versionStr, ok := form.Value["version"] var version int64 if ok && len(versionStr) > 0 { version, err = strconv.ParseInt(versionStr[0], 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("invalid version:%s", err.Error())) } } var ( answersRaw, answers []model.Answer errs []error ) deviceType := c.Get("DeviceType") os := c.Get("OS") browser := c.Get("Browser") ip := c.IP() device := c.Get("Device") if err := json.Unmarshal([]byte(answersStr[0]), &answersRaw); err != nil { return c.Status(fiber.StatusBadRequest).SendString("not valid answers string") } quizID, ok := form.Value["qid"] if !ok { return c.Status(fiber.StatusFailedDependency).SendString("no quiz id provided") } fp := "" if cfp := c.Cookies(fingerprintCookie); cfp != "" { fp = cfp } quiz, err := s.dal.GetQuizByQid(c.Context(), quizID[0]) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can not get quiz") } fileIDMap := make(map[uint64]string) for _, ans := range answersRaw { final := false if finalStr, exists := form.Value["final"]; exists && len(finalStr) > 0 { parsedFinal, err := strconv.ParseBool(finalStr[0]) if err != nil { return c.Status(fiber.StatusBadRequest).SendString("invalid final value") } final = parsedFinal } question, err := s.dal.GetQuestionListByIDs(c.Context(), []int32{int32(ans.QuestionId)}) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can not get questions") } if len(question) == 0 { return c.Status(fiber.StatusNotFound).SendString("no questions found") } questionText, err := s.aiClient.SendAnswerer(final, question[0].Type, ans.Content, cs) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed send answer to ai, err: %s", err.Error())) } _, err = s.dal.CreateQuestion(c.Context(), &model.Question{ QuizId: quiz.Id, Title: truncateUTF8(questionText, 512), Type: model.TypeText, Session: cs, Content: `{"id":"gcZ-9SET-sM6stZSzQMmu","hint":{"text":" ","video":" "},"rule":{"children":[],"main":[],"parentId":" ","default":" "},"back":" ","originalBack":" ","autofill":false,"placeholder":" ","innerNameCheck":false,"innerName":" ","required":false,"answerType":"single","onlyNumbers":false}`, Description: questionText, }) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed create question type ai, err: %s", err.Error())) } ans.DeviceType = deviceType ans.OS = os ans.Browser = browser ans.IP = ip ans.Device = device ans.Version = int32(version) if strings.HasPrefix(ans.Content, filePrefix) { filekey := strings.TrimPrefix(ans.Content, filePrefix) filenameparts := strings.Split(filekey, ".") filetail := filenameparts[len(filenameparts)-1] fileparts := form.File[filekey] if len(fileparts) == 0 { errs = append(errs, fmt.Errorf("no parts for file: %s", filekey)) continue } fname := fmt.Sprintf("%s.%s", xid.New().String(), filetail) ans.Content = fname fileIDMap[ans.QuestionId] = fname } ans.Session = cs ans.QuizId = quiz.Id ans.CreatedAt = time.Now() answers = append(answers, ans) if ans.Result { content := model.ResultContent{} err := json.Unmarshal([]byte(ans.Content), &content) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("error unmarshalling answer content: " + err.Error()) } ans.Email = content.Email } } quizConfig := model.QuizConfig{} err = json.Unmarshal([]byte(quiz.Config), &quizConfig) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can not unmarshal quiz config") } stored, ers := s.dal.CreateAnswers(c.Context(), answers, cs, fp, quiz.Id) if len(ers) != 0 { for _, err := range ers { if strings.Contains(err.Error(), "duplicate key value") { return c.Status(fiber.StatusAlreadyReported).SendString("User has already passed the quiz") } } return c.Status(fiber.StatusInternalServerError).SendString("some errors are casualted: " + fmt.Sprint(ers)) } var questionIDs []uint64 for _, ans := range stored { questionIDs = append(questionIDs, ans.QuestionId) } response := PutAnswersResponse{ FileIDMap: fileIDMap, Stored: questionIDs, } return c.Status(fiber.StatusOK).JSON(response) } func truncateUTF8(s string, maxLen int) string { if utf8.RuneCountInString(s) <= maxLen { return s } // Конвертируем строку в руны для корректной обработки Unicode runes := []rune(s) return string(runes[:maxLen]) }