answerer/service/service.go

509 lines
14 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"
"net/url"
"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"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/utils"
"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
}
type ServiceDeps struct {
Store *dal.Storer
Dal *quizdal.DAL
WorkerRespondentCh chan<- []model.Answer
WorkerSendClientCh chan<- model.Answer
Encrypt *utils.Encrypt
RedirectURl string
}
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,
}
}
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
}
// 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
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,
})
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,
}
}
// 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")
}
version, err := strconv.ParseInt(form.Value["version"][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 {
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)
}