281 lines
7.7 KiB
Go
281 lines
7.7 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"
|
|
"gitea.pena/SQuiz/common/utils"
|
|
"github.com/gofiber/fiber/v2"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/rs/xid"
|
|
)
|
|
|
|
const (
|
|
quizIdCookie = "qud"
|
|
fingerprintCookie = "fp"
|
|
)
|
|
|
|
type Service struct {
|
|
dal *quizdal.DAL
|
|
batch []model.Answer
|
|
m sync.Mutex
|
|
encrypt *utils.Encrypt
|
|
redirectURl string
|
|
aiClient *clients.AIClient
|
|
quizConfigData GetQuizDataResp
|
|
}
|
|
|
|
type ServiceDeps struct {
|
|
Dal *quizdal.DAL
|
|
Encrypt *utils.Encrypt
|
|
RedirectURl string
|
|
AiClient *clients.AIClient
|
|
}
|
|
|
|
func New(deps ServiceDeps) (*Service, error) {
|
|
quizData, err := loadQuizDataConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Service{
|
|
dal: deps.Dal,
|
|
m: sync.Mutex{},
|
|
batch: []model.Answer{},
|
|
encrypt: deps.Encrypt,
|
|
redirectURl: deps.RedirectURl,
|
|
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])
|
|
}
|