Compare commits

...

4 Commits

Author SHA1 Message Date
c901e52844 added normal json file format 2025-05-29 17:57:18 +03:00
1cfb0f1b86 update frontend_model.go and GetScriptTemplate 2025-05-27 16:25:11 +03:00
3164b42bc4 upd 2025-05-27 12:25:41 +03:00
988df9d658 start create controllers and structs 2025-05-22 18:49:56 +03:00
2 changed files with 378 additions and 0 deletions

196
service/frontend_model.go Normal file

@ -0,0 +1,196 @@
package service
import (
"encoding/json"
"fmt"
"gitea.pena/SQuiz/common/model"
)
type Props struct {
QuizSettings *QuizSettings `json:"quizSettings,omitempty"`
QuizID string `json:"quizId"`
//Preview *bool `json:"preview,omitempty"`
//ChangeFaviconAndTitle *bool `json:"changeFaviconAndTitle,omitempty"`
//ClassName *string `json:"className,omitempty"`
//DisableGlobalCss *bool `json:"disableGlobalCss,omitempty"`
}
type QuizSettings struct {
Questions []Question `json:"questions"`
Settings Settings `json:"settings"`
Cnt uint64 `json:"cnt"`
RecentlyCompleted bool `json:"recentlyCompleted"`
ShowBadge bool `json:"show_badge"`
}
type Question struct {
BackendID int `json:"backendId"`
ID string `json:"id"`
QuizID int `json:"quizId"`
Title string `json:"title"`
Description string `json:"description"`
Page int `json:"page"`
Type *string `json:"type"`
Expanded bool `json:"expanded"`
OpenedModalSettings bool `json:"openedModalSettings"`
Deleted bool `json:"deleted"`
Required bool `json:"required"`
DeleteTimeoutID int `json:"deleteTimeoutId"`
Content Content `json:"content"`
}
type Content struct {
ID string `json:"id"`
Hint Hint `json:"hint"`
Rule Rule `json:"rule"`
Back *string `json:"back"`
OriginalBack *string `json:"originalBack"`
Autofill bool `json:"autofill"`
}
type Hint struct {
Text string `json:"text"`
Video string `json:"video"`
}
type Rule struct {
Children []interface{} `json:"children"`
Default string `json:"default"`
Main []interface{} `json:"main"`
ParentID string `json:"parentId"`
}
type Settings struct {
FP bool `json:"fp"`
REP bool `json:"rep"`
Name string `json:"name"`
CFG Config `json:"cfg"`
LIM int `json:"lim"`
DUE int `json:"due"`
Delay int `json:"delay"`
Pausable bool `json:"pausable"`
Status string `json:"status"` // "start" | "stop" | "ai"
}
type Config struct {
IsUnSc *bool `json:"isUnSc,omitempty"`
Spec *bool `json:"spec,omitempty"`
Type *string `json:"type"` // "quiz" | "form" | null
NoStartPage bool `json:"noStartPage"`
StartpageType *string `json:"startpageType"` // "standard" | "expanded" | "centered"
Score *bool `json:"score,omitempty"`
Results *bool `json:"results"`
HaveRoot *string `json:"haveRoot"`
Theme string `json:"theme"`
Design bool `json:"design"`
ResultInfo ResultInfo `json:"resultInfo"`
Startpage Startpage `json:"startpage"`
FormContact FormContact `json:"formContact"`
Info Info `json:"info"`
Meta string `json:"meta"`
Antifraud *bool `json:"antifraud,omitempty"`
Showfc *bool `json:"showfc,omitempty"`
YandexMetricsNumber *int `json:"yandexMetricsNumber,omitempty"`
VkMetricsNumber *int `json:"vkMetricsNumber,omitempty"`
}
type ResultInfo struct {
When string `json:"when"` // "email" | ""
Share bool `json:"share"`
Replay bool `json:"replay"`
Theme string `json:"theme"`
Reply string `json:"reply"`
Replname string `json:"replname"`
ShowResultForm string `json:"showResultForm"` // "before" | "after"
}
type Startpage struct {
Description string `json:"description"`
Button string `json:"button"`
Position string `json:"position"` // "left" | "right" | "center"
FavIcon *string `json:"favIcon"`
Logo *string `json:"logo"`
OriginalLogo *string `json:"originalLogo"`
Background Background `json:"background"`
}
type Background struct {
Type *string `json:"type"` // null | "image" | "video"
Desktop *string `json:"desktop"`
OriginalDesktop *string `json:"originalDesktop"`
Mobile *string `json:"mobile"`
OriginalMobile *string `json:"originalMobile"`
Video *string `json:"video"`
Cycle bool `json:"cycle"`
}
type FormContact struct {
Title string `json:"title"`
Desc string `json:"desc"`
Fields map[string]interface{} `json:"fields"`
Button string `json:"button"`
}
type Info struct {
PhoneNumber string `json:"phonenumber"`
Clickable bool `json:"clickable"`
OrgName string `json:"orgname"`
Site string `json:"site"`
Law *string `json:"law,omitempty"`
}
func dao2dtoProps(quiz model.Quiz, questions []model.Question, showBadge bool, cnt uint64) Props {
var cfg Config
err := json.Unmarshal([]byte(quiz.Config), &cfg)
if err != nil {
fmt.Printf("failed to unmarshal quiz.Config: %v\n", err)
cfg = Config{}
}
return Props{
QuizID: quiz.Qid,
QuizSettings: &QuizSettings{
Settings: Settings{
FP: quiz.Fingerprinting,
REP: quiz.Repeatable,
Name: quiz.Name,
CFG: cfg,
LIM: int(quiz.Limit),
DUE: int(quiz.DueTo),
Delay: int(quiz.TimeOfPassing),
Pausable: quiz.Pausable,
Status: quiz.Status,
},
Cnt: cnt,
ShowBadge: showBadge,
Questions: dao2dtoQuestions(int(quiz.Id), questions),
},
}
}
func dao2dtoQuestions(quizId int, questions []model.Question) []Question {
var res []Question
for _, q := range questions {
var content Content
err := json.Unmarshal([]byte(q.Content), &content)
if err != nil {
fmt.Printf("failed to unmarshal question.Content for ID %d: %v\n", q.Id, err)
content = Content{}
}
t := q.Type
res = append(res, Question{
BackendID: int(q.Id),
//ID: "",
QuizID: quizId,
Title: q.Title,
Description: q.Description,
Page: q.Page,
Type: &t,
Deleted: q.Deleted,
Required: q.Required,
Content: content,
})
}
return res
}

@ -12,6 +12,7 @@ import (
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/utils"
"github.com/gofiber/fiber/v2"
"io"
"net/url"
"strconv"
"strings"
@ -36,6 +37,8 @@ type Service struct {
encrypt *utils.Encrypt
redirectURl string
aiClient *clients.AIClient
script scriptTemplate
}
type ServiceDeps struct {
@ -48,6 +51,11 @@ type ServiceDeps struct {
AiClient *clients.AIClient
}
type scriptTemplate struct {
full []byte
mu sync.RWMutex
}
func New(deps ServiceDeps) *Service {
return &Service{
store: deps.Store,
@ -66,6 +74,9 @@ func (s *Service) Register(app *fiber.App) *fiber.App {
app.Post("/answer", s.PutAnswersOnePiece)
app.Post("/settings", s.GetQuizData)
app.Get("/logo", s.MiniPart)
app.Put("/pub.js", s.UploadScriptTemplate)
app.Get("/pub/:qID.js", s.GetScriptTemplate)
return app
}
@ -563,3 +574,174 @@ func (s *Service) MiniPart(ctx *fiber.Ctx) error {
return ctx.Redirect(s.redirectURl, fiber.StatusFound)
}
const marker = "window.__CONFIG__"
func (s *Service) UploadScriptTemplate(ctx *fiber.Ctx) error {
fileHeader, err := ctx.FormFile("pub.js")
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("failed to get file: " + err.Error())
}
file, err := fileHeader.Open()
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to open file: " + err.Error())
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to read file: " + err.Error())
}
s.script.mu.Lock()
s.script.full = content
s.script.mu.Unlock()
return ctx.SendStatus(fiber.StatusNoContent)
}
// возвращаем файл pub.js начало marker = маршаленные данные структуры Props и s.script.full
func (s *Service) GetScriptTemplate(ctx *fiber.Ctx) error {
cs, ok := ctx.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("no session in cookie")
}
qID := ctx.Params("qID")
if qID == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("missing qID in path")
}
hlogger := log_mw.ExtractLogger(ctx)
quiz, err := s.dal.QuizRepo.GetQuizByQid(ctx.Context(), qID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if quiz.Status != model.StatusStart && quiz.Status != model.StatusAI {
return ctx.Status(fiber.StatusLocked).SendString("quiz is inactive")
}
if quiz.DueTo < uint64(time.Now().Unix()) && quiz.DueTo > 0 {
return ctx.Status(fiber.StatusGone).SendString("quiz timeouted")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), quiz.AccountId)
if err != nil {
return ctx.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 {
questions, cnt, err = s.dal.QuestionRepo.GetQuestionsAI(ctx.Context(), int64(quiz.Id), cs, 100_000, 0, 0)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
} else {
questions, cnt, err = s.dal.QuestionRepo.GetQuestionList(
ctx.Context(),
100_000, 0, 0, 0, quiz.Id, false, false, "", "", 0,
)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
}
utmData := model.UTMSavingMap{
"utm_content": ctx.Query("utm_content"),
"utm_medium": ctx.Query("utm_medium"),
"utm_campaign": ctx.Query("utm_campaign"),
"utm_source": ctx.Query("utm_source"),
"utm_term": ctx.Query("utm_term"),
"utm_referrer": ctx.Query("utm_referrer"),
"roistat": ctx.Query("roistat"),
"referrer": ctx.Query("referrer"),
"openstat_service": ctx.Query("openstat_service"),
"openstat_campaign": ctx.Query("openstat_campaign"),
"openstat_ad": ctx.Query("openstat_ad"),
"openstat_source": ctx.Query("openstat_source"),
"from": ctx.Query("from"),
"gclientid": ctx.Query("gclientid"),
"_ym_uid": ctx.Query("_ym_uid"),
"_ym_counter": ctx.Query("_ym_counter"),
"gclid": ctx.Query("gclid"),
"yclid": ctx.Query("yclid"),
"fbclid": ctx.Query("fbclid"),
}
deviceType := ctx.Get("DeviceType")
os := ctx.Get("OS")
browser := ctx.Get("Browser")
ip := ctx.IP()
device := ctx.Get("Device")
referrer := ctx.Get("Referer")
fp := ""
if cfp := ctx.Cookies(fingerprintCookie); cfp != "" {
fp = cfp
}
if len(questions) == 0 {
return ctx.Status(fiber.StatusNotFound).SendString("question not found")
}
answers, errs := s.dal.AnswerRepo.CreateAnswers(ctx.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 ctx.Status(fiber.StatusInternalServerError).SendString(errs[0].Error())
}
hlogger.Emit(models.InfoQuizOpen{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: qID,
CtxQuizID: int64(quiz.Id),
CtxReferrer: referrer,
CtxIDInt: int64(answers[0].Id),
CtxSession: cs,
})
props := dao2dtoProps(quiz, questions, showBadge, cnt)
propsJSON, err := json.Marshal(props)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to marshal props")
}
propsJSONString, err := json.Marshal(string(propsJSON))
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to escape props JSON")
}
s.script.mu.RLock()
defer s.script.mu.RUnlock()
script := fmt.Sprintf("%s = %s;\n%s", marker, string(propsJSONString), s.script.full)
ctx.Set("Content-Disposition", `attachment; filename="pub.js"`)
return ctx.Type("application/javascript").Status(fiber.StatusOK).SendString(script)
}