answerer/service/service.go
2025-07-09 14:43:14 +03:00

269 lines
7.3 KiB
Go

package service
import (
"encoding/json"
"fmt"
"gitea.pena/SQuiz/answerer/clients"
quizdal "gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/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.QuizRepo.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.QuestionRepo.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.QuestionRepo.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.AnswerRepo.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])
}