package service import ( "encoding/json" "fmt" "github.com/gofiber/fiber/v2" "penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw" "penahub.gitlab.yandexcloud.net/backend/quiz/answerer/dal" "penahub.gitlab.yandexcloud.net/backend/quiz/answerer/models" quizdal "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal" "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware" "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model" "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 } func New(s *dal.Storer, q *quizdal.DAL, workerRespondentCh chan<- []model.Answer, workerSendClientCh chan<- model.Answer) *Service { return &Service{ store: s, dal: q, m: sync.Mutex{}, batch: []model.Answer{}, workerRespondentCh: workerRespondentCh, workerSendClientCh: workerSendClientCh, } } func (s *Service) Register(app *fiber.App) *fiber.App { app.Post("/answer", s.PutAnswersOnePiece) app.Post("/settings", s.GetQuizData) 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 } // GetQuizDataResp response with prepared data for user type GetQuizDataResp struct { Settings ShavedQuiz `json:"settings"` 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"` } // 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 { 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 { return c.Status(fiber.StatusOK).JSON(GetQuizDataResp{ Settings: dao2dtoQuiz(quiz), }) } if quiz.UniqueAnswers { //todo implement after creating store answers } if quiz.Status != model.StatusStart { 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 fmt.Println("PRIVRRRR", account.ID, account.Privileges) 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 } } questions, cnt, err := s.dal.QuestionRepo.GetQuestionList( c.Context(), req.Limit, req.Page*req.Limit, 0, 0, quiz.Id, false, false, "", "", ) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } result := GetQuizDataResp{ Count: cnt, Items: []ShavedQuestion{}, ShowBadge: showBadge, } if req.NeedConfig { result.Settings = dao2dtoQuiz(quiz) } 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 } cs, ok := c.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string) if !ok { return c.Status(fiber.StatusUnauthorized).SendString("no session in cookie") } 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, }) 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, } } // 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") } 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 { ans.DeviceType = deviceType ans.OS = os ans.Browser = browser ans.IP = ip ans.Device = device 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 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) }