answerer/service/service.go

445 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/answerer.git/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: quiz.Id,
CtxReferrer: referrer,
CtxID: 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: quiz.Id,
CtxReferrer: referrer,
CtxQuestionID: ans.QuestionId,
CtxID: ans.Id,
CtxSession: cs,
})
continue
}
hlogger.Emit(models.InfoAnswer{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: quizID[0],
CtxQuizID: quiz.Id,
CtxReferrer: referrer,
CtxQuestionID: ans.QuestionId,
CtxID: ans.Id,
CtxSession: cs,
})
}
response := PutAnswersResponse{
FileIDMap: fileIDMap,
Stored: questionIDs,
}
return c.Status(fiber.StatusOK).JSON(response)
}