answerer/service/service.go

281 lines
7.7 KiB
Go
Raw Normal View History

2024-02-19 18:27:12 +00:00
package service
import (
"encoding/json"
"fmt"
2025-04-23 15:39:15 +00:00
"gitea.pena/SQuiz/answerer/clients"
2025-02-05 22:11:54 +00:00
quizdal "gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/utils"
2024-02-19 18:27:12 +00:00
"github.com/gofiber/fiber/v2"
2024-09-18 16:34:28 +00:00
"strconv"
2024-02-19 18:27:12 +00:00
"strings"
"sync"
"time"
2025-07-09 11:40:02 +00:00
"unicode/utf8"
2024-02-19 18:27:12 +00:00
"github.com/rs/xid"
)
const (
quizIdCookie = "qud"
fingerprintCookie = "fp"
)
type Service struct {
2025-07-09 11:40:02 +00:00
dal *quizdal.DAL
batch []model.Answer
m sync.Mutex
encrypt *utils.Encrypt
redirectURl string
aiClient *clients.AIClient
quizConfigData GetQuizDataResp
2024-02-19 18:27:12 +00:00
}
2024-06-01 13:20:13 +00:00
type ServiceDeps struct {
2025-07-09 11:40:02 +00:00
Dal *quizdal.DAL
Encrypt *utils.Encrypt
RedirectURl string
AiClient *clients.AIClient
2024-06-01 13:20:13 +00:00
}
2025-07-09 11:40:02 +00:00
func New(deps ServiceDeps) (*Service, error) {
quizData, err := loadQuizDataConfig()
if err != nil {
return nil, err
2024-02-19 18:27:12 +00:00
}
2025-07-09 11:40:02 +00:00
return &Service{
dal: deps.Dal,
m: sync.Mutex{},
batch: []model.Answer{},
encrypt: deps.Encrypt,
redirectURl: deps.RedirectURl,
aiClient: deps.AiClient,
quizConfigData: quizData,
}, nil
2024-02-19 18:27:12 +00:00
}
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"`
2024-03-13 17:04:47 +00:00
Items []ShavedQuestion `json:"items"`
Count uint64 `json:"cnt"`
ShowBadge bool `json:"show_badge"`
2024-02-19 18:27:12 +00:00
}
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"`
2025-04-23 15:39:15 +00:00
Status string `json:"status"`
2024-02-19 18:27:12 +00:00
}
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 {
2025-07-09 11:40:02 +00:00
return c.Status(fiber.StatusOK).JSON(s.quizConfigData)
2024-02-19 18:27:12 +00:00
}
// 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")
}
2024-09-22 17:12:11 +00:00
versionStr, ok := form.Value["version"]
var version int64
2024-09-22 17:12:11 +00:00
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()))
}
2024-09-18 16:34:28 +00:00
}
2024-02-19 18:27:12 +00:00
var (
2025-07-09 11:40:02 +00:00
answersRaw, answers []model.Answer
errs []error
2024-02-19 18:27:12 +00:00
)
2024-03-14 13:13:17 +00:00
deviceType := c.Get("DeviceType")
os := c.Get("OS")
browser := c.Get("Browser")
ip := c.IP()
device := c.Get("Device")
2024-03-14 13:13:17 +00:00
2024-02-19 18:27:12 +00:00
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 {
2025-07-09 11:40:02 +00:00
final := false
if finalStr, exists := form.Value["final"]; exists && len(finalStr) > 0 {
parsedFinal, err := strconv.ParseBool(finalStr[0])
if err != nil {
2025-07-09 11:40:02 +00:00
return c.Status(fiber.StatusBadRequest).SendString("invalid final value")
}
2025-07-09 11:40:02 +00:00
final = parsedFinal
}
2025-07-09 11:40:02 +00:00
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")
}
2025-07-09 11:40:02 +00:00
if len(question) == 0 {
return c.Status(fiber.StatusNotFound).SendString("no questions found")
}
2025-07-09 11:40:02 +00:00
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()))
}
2024-03-14 13:13:17 +00:00
ans.DeviceType = deviceType
ans.OS = os
ans.Browser = browser
ans.IP = ip
ans.Device = device
2024-09-18 16:34:28 +00:00
ans.Version = int32(version)
2024-02-19 18:27:12 +00:00
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)
2025-07-09 11:40:02 +00:00
2024-02-19 18:27:12 +00:00
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 {
2024-03-13 17:04:47 +00:00
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
2024-02-19 18:27:12 +00:00
}
}
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 {
2024-03-13 17:04:47 +00:00
for _, err := range ers {
if strings.Contains(err.Error(), "duplicate key value") {
return c.Status(fiber.StatusAlreadyReported).SendString("User has already passed the quiz")
}
}
2024-02-19 18:27:12 +00:00
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)
}
2024-02-19 18:27:12 +00:00
response := PutAnswersResponse{
FileIDMap: fileIDMap,
Stored: questionIDs,
2024-02-19 18:27:12 +00:00
}
return c.Status(fiber.StatusOK).JSON(response)
}
2024-06-01 13:20:13 +00:00
2025-07-08 18:10:42 +00:00
func truncateUTF8(s string, maxLen int) string {
if utf8.RuneCountInString(s) <= maxLen {
return s
}
2025-07-09 11:40:02 +00:00
2025-07-08 18:10:42 +00:00
// Конвертируем строку в руны для корректной обработки Unicode
runes := []rune(s)
return string(runes[:maxLen])
}