502 lines
14 KiB
Go
502 lines
14 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"gitea.pena/PenaSide/common/log_mw"
|
||
"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"
|
||
"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")
|
||
}
|
||
|
||
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
|
||
|
||
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)
|
||
}
|