package service import ( "encoding/json" "fmt" "gitea.pena/PenaSide/common/log_mw" "gitea.pena/SQuiz/answerer/clients" "gitea.pena/SQuiz/answerer/dal" "gitea.pena/SQuiz/answerer/models" 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" "net/url" "strconv" "strings" "sync" "time" "github.com/rs/xid" ) const ( quizIdCookie = "qud" fingerprintCookie = "fp" ) type Service struct { store *dal.Storer dal *quizdal.DAL batch []model.Answer m sync.Mutex workerRespondentCh chan<- []model.Answer workerSendClientCh chan<- model.Answer encrypt *utils.Encrypt redirectURl string aiClient *clients.AIClient } type ServiceDeps struct { Store *dal.Storer Dal *quizdal.DAL WorkerRespondentCh chan<- []model.Answer WorkerSendClientCh chan<- model.Answer Encrypt *utils.Encrypt RedirectURl string AiClient *clients.AIClient } func New(deps ServiceDeps) *Service { return &Service{ store: deps.Store, dal: deps.Dal, m: sync.Mutex{}, batch: []model.Answer{}, workerRespondentCh: deps.WorkerRespondentCh, workerSendClientCh: deps.WorkerSendClientCh, encrypt: deps.Encrypt, redirectURl: deps.RedirectURl, aiClient: deps.AiClient, } } func (s *Service) Register(app *fiber.App) *fiber.App { app.Post("/answer", s.PutAnswersOnePiece) app.Post("/settings", s.GetQuizData) app.Get("/logo", s.MiniPart) return app } // GetQuizDataReq request data for get data for user type GetQuizDataReq struct { QuizId string `json:"quiz_id"` // relation to quiz Limit uint64 `json:"limit"` Page uint64 `json:"page"` NeedConfig bool `json:"need_config"` // true if you need not only question page Auditory int64 `json:"auditory"` } // GetQuizDataResp response with prepared data for user type GetQuizDataResp struct { Settings *ShavedQuiz `json:"settings,omitempty"` Items []ShavedQuestion `json:"items"` Count uint64 `json:"cnt"` ShowBadge bool `json:"show_badge"` } // ShavedQuiz shortened struct for delivery data to customer 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"` } // ShavedQuestion shortened struct for delivery data to customer 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 { cs, ok := c.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string) if !ok { return c.Status(fiber.StatusUnauthorized).SendString("no session in cookie") } hlogger := log_mw.ExtractLogger(c) var req GetQuizDataReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid request data") } if req.QuizId == "" { return c.Status(fiber.StatusBadRequest).SendString("Invalid request data") } if req.Limit == 0 && !req.NeedConfig { return c.Status(fiber.StatusLengthRequired).SendString("no data requested") } quiz, err := s.dal.QuizRepo.GetQuizByQid(c.Context(), req.QuizId) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } if req.Limit == 0 && req.NeedConfig { quizDto := dao2dtoQuiz(quiz) return c.Status(fiber.StatusOK).JSON(GetQuizDataResp{ Settings: &quizDto, }) } if quiz.UniqueAnswers { //todo implement after creating store answers } if quiz.Status != model.StatusStart && quiz.Status != model.StatusAI { return c.Status(fiber.StatusLocked).SendString("quiz is inactive") } if quiz.Limit > 0 { // todo implement after creating store answer } if quiz.DueTo < uint64(time.Now().Unix()) && quiz.DueTo > 0 { return c.Status(fiber.StatusGone).SendString("quiz timeouted") } account, err := s.dal.AccountRepo.GetAccountByID(c.Context(), quiz.AccountId) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can`t get account by quiz.AccountId") } showBadge := true if priv, ok := account.Privileges["squizHideBadge"]; ok { expiration := priv.CreatedAt.Add(time.Duration(priv.Amount) * 24 * time.Hour) if time.Now().Before(expiration) { showBadge = false } } var questions []model.Question var cnt uint64 if quiz.Status == model.StatusAI { if req.Page >= 1 { req.Page+=1 } questions, cnt, err = s.dal.QuestionRepo.GetQuestionsAI(c.Context(), int64(quiz.Id), cs, int32(req.Limit), int32(req.Page*req.Limit), req.Auditory) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } } else { questions, cnt, err = s.dal.QuestionRepo.GetQuestionList( c.Context(), req.Limit, req.Page*req.Limit, 0, 0, quiz.Id, false, false, "", "", req.Auditory, ) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } } result := GetQuizDataResp{ Count: cnt, Items: []ShavedQuestion{}, ShowBadge: showBadge, } if req.NeedConfig { quizDto := dao2dtoQuiz(quiz) result.Settings = &quizDto } for _, q := range questions { result.Items = append(result.Items, dao2dtoQuestion(q)) } utmData := model.UTMSavingMap{ "utm_content": c.Query("utm_content"), "utm_medium": c.Query("utm_medium"), "utm_campaign": c.Query("utm_campaign"), "utm_source": c.Query("utm_source"), "utm_term": c.Query("utm_term"), "utm_referrer": c.Query("utm_referrer"), "roistat": c.Query("roistat"), "referrer": c.Query("referrer"), "openstat_service": c.Query("openstat_service"), "openstat_campaign": c.Query("openstat_campaign"), "openstat_ad": c.Query("openstat_ad"), "openstat_source": c.Query("openstat_source"), "from": c.Query("from"), "gclientid": c.Query("gclientid"), "_ym_uid": c.Query("_ym_uid"), "_ym_counter": c.Query("_ym_counter"), "gclid": c.Query("gclid"), "yclid": c.Query("yclid"), "fbclid": c.Query("fbclid"), } deviceType := c.Get("DeviceType") os := c.Get("OS") browser := c.Get("Browser") ip := c.IP() device := c.Get("Device") referrer := c.Get("Referer") fp := "" if cfp := c.Cookies(fingerprintCookie); cfp != "" { fp = cfp } if req.NeedConfig { if len(questions) == 0 { if req.Auditory != 0 { return c.Status(fiber.StatusAccepted).SendString("questions are not ready yet") } return c.Status(fiber.StatusNotFound).SendString("question not found") } answers, errs := s.dal.AnswerRepo.CreateAnswers(c.Context(), []model.Answer{{ Content: "start", QuestionId: questions[0].Id, QuizId: quiz.Id, Start: true, DeviceType: deviceType, Device: device, Browser: browser, IP: ip, OS: os, Utm: utmData, }}, cs, fp, quiz.Id) if len(errs) != 0 { return c.Status(fiber.StatusInternalServerError).SendString(errs[0].Error()) } hlogger.Emit(models.InfoQuizOpen{ KeyOS: os, KeyDevice: device, KeyDeviceType: deviceType, KeyBrowser: browser, CtxQuiz: req.QuizId, CtxQuizID: int64(quiz.Id), CtxReferrer: referrer, CtxIDInt: int64(answers[0].Id), CtxSession: cs, }) } fmt.Println("SETTIIIIII", cnt <= req.Limit, result) if cnt <= req.Limit { return c.Status(fiber.StatusOK).JSON(result) } else { return c.Status(fiber.StatusPartialContent).JSON(result) } } func dao2dtoQuestion(question model.Question) ShavedQuestion { return ShavedQuestion{ Id: question.Id, Title: question.Title, Description: question.Description, Type: question.Type, Required: false, Page: question.Page, Content: question.Content, } } func dao2dtoQuiz(quiz model.Quiz) ShavedQuiz { return ShavedQuiz{ Fingerprinting: quiz.Fingerprinting, Repeatable: quiz.Repeatable, Name: quiz.Name, Config: quiz.Config, Limit: quiz.Limit, DueTo: quiz.DueTo, TimeOfPassing: quiz.TimeOfPassing, Pausable: quiz.Pausable, Status: quiz.Status, } } // MB Size constants const ( MB = 1 << 20 filePrefix = "file:" ) type PutAnswersResponse struct { FileIDMap map[uint64]string `json:"fileIDMap"` Stored []uint64 `json:"stored"` } // todo не отдавать ответы с типом start но сохранять брать из контента ответа // поле бул также в GetQuizdata не отдавать его, но по запросам идет GetQuizdata получаем данные квиза // аотом отправляем ответы в PutAnswers и сохраяняем их подумать как делать 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") } hlogger := log_mw.ExtractLogger(c) 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, trueRes []model.Answer errs []error ) deviceType := c.Get("DeviceType") os := c.Get("OS") browser := c.Get("Browser") ip := c.IP() device := c.Get("Device") referrer := c.Get("Referer") 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 { if quiz.Status == model.StatusAI { 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: " ", Type: model.TypeText, Session: cs, 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 } r, err := fileparts[0].Open() if err != nil { errs = append(errs, fmt.Errorf("can not open part for file: %s", filekey)) continue } fname := fmt.Sprintf("%s.%s", xid.New().String(), filetail) if err := s.store.PutAnswer(c.Context(), r, quizID[0], fname, ans.QuestionId, fileparts[0].Size); err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can not upload file answers") } 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 hlogger.Emit(models.InfoContactForm{ KeyOS: os, KeyDevice: device, KeyDeviceType: deviceType, KeyBrowser: browser, CtxQuiz: quizID[0], CtxQuizID: int64(quiz.Id), CtxReferrer: referrer, CtxQuestionID: int64(ans.QuestionId), CtxIDInt: int64(ans.Id), CtxSession: cs, }) s.workerSendClientCh <- ans trueRes = append(trueRes, ans) } } quizConfig := model.QuizConfig{} err = json.Unmarshal([]byte(quiz.Config), &quizConfig) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("can not unmarshal quiz config") } if quizConfig.Mailing.When == "email" && len(trueRes) > 0 { s.workerRespondentCh <- trueRes } 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) if ans.Result { hlogger.Emit(models.InfoResult{ KeyOS: os, KeyDevice: device, KeyDeviceType: deviceType, KeyBrowser: browser, CtxQuiz: quizID[0], CtxQuizID: int64(quiz.Id), CtxReferrer: referrer, CtxQuestionID: int64(ans.QuestionId), CtxIDInt: int64(ans.Id), CtxSession: cs, }) continue } hlogger.Emit(models.InfoAnswer{ KeyOS: os, KeyDevice: device, KeyDeviceType: deviceType, KeyBrowser: browser, CtxQuiz: quizID[0], CtxQuizID: int64(quiz.Id), CtxReferrer: referrer, CtxQuestionID: int64(ans.QuestionId), CtxIDInt: int64(ans.Id), CtxSession: cs, }) } response := PutAnswersResponse{ FileIDMap: fileIDMap, Stored: questionIDs, } return c.Status(fiber.StatusOK).JSON(response) } func (s *Service) MiniPart(ctx *fiber.Ctx) error { qid := ctx.Query("q") if qid == "" { return ctx.Status(fiber.StatusBadRequest).SendString("qid is nil") } ctx.Cookie(&fiber.Cookie{ Name: "quizFrom", Value: qid, }) userID, err := s.dal.AccountRepo.GetQidOwner(ctx.Context(), qid) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } shifr, err := s.encrypt.EncryptStr(userID) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } ctx.Cookie(&fiber.Cookie{ Name: "quizUser", Value: url.QueryEscape(string(shifr)), }) return ctx.Redirect(s.redirectURl, fiber.StatusFound) }