diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1ffb511
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+squiz
+.idea/
+gen
+worker/worker
+storer/storer
+answerer/answerer
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..92fc7b5
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/golang as build
+WORKDIR /app
+COPY . .
+ARG GITLAB_TOKEN
+RUN echo ${GITLAB_TOKEN}
+ENV GOPRIVATE=penahub.gitlab.yandexcloud.net/backend/penahub_common
+RUN git config --global url."https://buildToken:glpat-axA8ttckx3aPf_xd2Dym@penahub.gitlab.yandexcloud.net/".insteadOf "https://penahub.gitlab.yandexcloud.net/"
+RUN go mod download
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o wrkr ./worker/main.go
+
+FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/alpine as prod
+COPY --from=build /app/wrkr .
+ENV IS_PROD_LOG=false
+ENV IS_PROD=false
+ENV PG_CRED="host=postgres port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"
+CMD ["/wrkr"]
diff --git a/answerwc/mail/reminder.tmpl b/answerwc/mail/reminder.tmpl
new file mode 100644
index 0000000..f966af0
--- /dev/null
+++ b/answerwc/mail/reminder.tmpl
@@ -0,0 +1,191 @@
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+ |
+
+ Квиз для вашего бизнеса
+ |
+
+
+
+
+
+ Поступила новая заявка с квиза “{{ .QuizConfig.Theme }}”!
+
+ |
+
+
+
+
+ Но у вас закончились средства на балансе :(
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ Аккаунт
+
+ |
+
+
+
+
+
+
+
+ Email
+ |
+
+
+ {{ .QuizConfig.Reply }}
+
+ |
+
+
+ |
+
+
+
+
+
+ Пополните баланс и посмотрите заявку в личном кабинете:
+
+ |
+
+
+
+
+
+ Посмотреть в личном кабинете
+
+ |
+
+
+
+
+
+
+ quiz.pena.digital
+
+ |
+
+
+
+
diff --git a/answerwc/mail/to_client.tmpl b/answerwc/mail/to_client.tmpl
new file mode 100644
index 0000000..071e014
--- /dev/null
+++ b/answerwc/mail/to_client.tmpl
@@ -0,0 +1,537 @@
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+ |
+
+ Квиз для вашего бизнеса
+ |
+
+
+
+
+
+ Поступила новая заявка с квиза “{{.QuizConfig.Theme}}”!
+
+ |
+
+
+
+
+ Время заявки: {{ .AnswerTime }}
+
+ |
+
+
+
+
+
+ Посмотреть в личном кабинете
+
+ |
+
+
+
+
+
+ Контакты
+
+ |
+
+
+
+
+
+
+
+ Имя
+ |
+
+
+ {{ .AnswerContent.Name}}
+
+ |
+
+ {{ if .AnswerContent.Email }}
+
+
+ Email
+ |
+
+
+ {{ .AnswerContent.Email }}
+
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Phone }}
+
+
+ Телефон
+ |
+
+ {{ .AnswerContent.Phone }}
+ |
+
+ {{ end }}
+
+ {{ if .AnswerContent.Telegram }}
+
+
+ Telegram
+ |
+
+ {{ .AnswerContent.Telegram }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Wechat }}
+
+
+ Wechat
+ |
+
+ {{ .AnswerContent.Wechat }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Viber }}
+
+
+ Viber
+ |
+
+ {{ .AnswerContent.Viber }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Vk }}
+
+
+ Vk
+ |
+
+ {{ .AnswerContent.Vk }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Skype }}
+
+
+ Skype
+ |
+
+ {{ .AnswerContent.Skype }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Whatsup }}
+
+
+ Whatsup
+ |
+
+ {{ .AnswerContent.Whatsup }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Messenger }}
+
+
+ Messenger
+ |
+
+ {{ .AnswerContent.Messenger }}
+ |
+
+ {{ end }}
+ {{ if .AnswerContent.Address }}
+
+
+ Адрес
+ |
+
+ {{ .AnswerContent.Address }}
+ |
+
+ {{ end }}
+ {{ range $key, $value := .AnswerContent.Custom }}
+
+
+
+ {{ $key }}
+ |
+
+ {{ $value }}
+ |
+
+ {{ end }}
+
+ |
+
+
+
+
+
+ Ответы
+
+ |
+
+
+ {{ range .AllAnswers }}
+ {{ if index $.QuestionsMap .AnswerID }}
+
+
+
+
+
+
+ {{ index $.QuestionsMap .AnswerID }}
+
+ |
+
+
+
+
+ {{ .Content }}
+ |
+
+
+ |
+
+ {{ end }}
+ {{end}}
+
+
+
+
+ quiz.pena.digital
+
+ |
+
+
+
+
diff --git a/answerwc/respondent.go b/answerwc/respondent.go
new file mode 100644
index 0000000..d2dc6a8
--- /dev/null
+++ b/answerwc/respondent.go
@@ -0,0 +1,170 @@
+package answerwc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/themakers/hlog"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/mailclient"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/wctools"
+ "time"
+)
+
+type DepsRespWorker struct {
+ Redis *redis.Client
+ Dal *dal.DAL
+ MailClient *mailclient.Client
+}
+
+type RespWorker struct {
+ deps DepsRespWorker
+ logger hlog.Logger
+ errChan chan<- error
+}
+
+func NewRespWorker(deps DepsRespWorker, logger hlog.Logger, errChan chan<- error) *RespWorker {
+ return &RespWorker{
+ deps: deps,
+ logger: logger,
+ errChan: errChan,
+ }
+}
+
+func (w *RespWorker) Start(ctx context.Context) {
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ w.processPendingAnswer(ctx)
+
+ case <-ctx.Done():
+ w.logger.Module("To respondent worker terminated")
+ return
+ }
+
+ }
+}
+
+func (w *RespWorker) processPendingAnswer(ctx context.Context) {
+ keys, err := w.deps.Redis.Keys(ctx, "toRespondent:*").Result()
+ if err != nil {
+ w.reportError(err, "Error retrieving keys from Redis")
+ return
+ }
+
+ for _, key := range keys {
+ func() {
+ answerJSON, err := w.deps.Redis.GetDel(ctx, key).Result()
+ if err == redis.Nil {
+ return
+ } else if err != nil {
+ w.reportError(err, "Error getting and deleting data from Redis")
+ return
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ w.reportError(nil, fmt.Sprintf("recovering from panic or error setting redis value %v", r))
+ _ = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
+ }
+ }()
+
+ var answer model.Answer
+ err = json.Unmarshal([]byte(answerJSON), &answer)
+ if err != nil {
+ w.reportError(err, "Error unmarshalling answer")
+ return
+ }
+
+ answerContent, err := wctools.ProcessAnswer(answer.Content)
+ if err != nil {
+ w.reportError(err, "Error processing answer content")
+ return
+ }
+
+ quizConfig, accountId, err := w.deps.Dal.QuizRepo.GetQuizConfig(ctx, answer.QuizId)
+ if err != nil {
+ w.reportError(err, "Error getting quiz config")
+ return
+ }
+
+ quiz, err := w.deps.Dal.QuizRepo.GetQuizById(ctx, accountId, answer.QuizId)
+ if err != nil {
+ w.reportError(err, "Error getting quiz")
+ return
+ }
+
+ quizConfig.Mailing.Reply = quiz.Name
+
+ if quizConfig.Mailing.Theme == "" {
+ quizConfig.Mailing.Theme = quiz.Name
+ }
+
+ allAnswers, err := w.deps.Dal.AnswerRepo.GetAllAnswersByQuizID(ctx, answer.Session)
+ if err != nil {
+ w.reportError(err, "Error getting all answers by quizID")
+ return
+ }
+
+ questionsMap, err := w.deps.Dal.QuestionRepo.GetMapQuestions(ctx, allAnswers)
+ if err != nil {
+ w.reportError(err, "Error getting questionsMap")
+ return
+ }
+
+ fmt.Println("ATATATA", questionsMap, allAnswers)
+
+ err = w.processMessageToSMTP(quizConfig, questionsMap, allAnswers, answerContent, answer.CreatedAt)
+ if err != nil {
+ w.reportError(err, "Error sending message to SMTP")
+ }
+ }()
+ }
+}
+
+func (w *RespWorker) processMessageToSMTP(quizConfig model.QuizConfig, questionsMap map[uint64]string,
+ allAnswers []model.ResultAnswer, answerContent model.ResultContent, answerTime time.Time) error {
+ theme := quizConfig.Mailing.Theme
+ quizConfig.Mailing.Theme = quizConfig.Mailing.Reply
+
+ data := mailclient.EmailTemplateData{
+ QuizConfig: quizConfig.Mailing,
+ AnswerContent: answerContent,
+ AllAnswers: allAnswers,
+ QuestionsMap: questionsMap,
+ }
+
+ dayOfWeek := wctools.DaysOfWeek[answerTime.Format("Monday")]
+ monthOfYear := wctools.MonthsOfYear[answerTime.Format("January")]
+
+ formattedTime := fmt.Sprintf("%s, %d %s %d г., %02d:%02d (UTC%s)",
+ dayOfWeek,
+ answerTime.Day(),
+ monthOfYear,
+ answerTime.Year(),
+ answerTime.Hour(),
+ answerTime.Minute(),
+ answerTime.Format("-07:00"),
+ )
+
+ data.AnswerTime = formattedTime
+
+ err := w.deps.MailClient.SendMailWithAttachment(answerContent.Email, theme, toClientTemplate, data, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (w *RespWorker) reportError(err error, message string) {
+ if err != nil {
+ fmt.Println(message + ": " + err.Error())
+ w.errChan <- err
+ }
+}
diff --git a/answerwc/to_client.go b/answerwc/to_client.go
new file mode 100644
index 0000000..3d5c802
--- /dev/null
+++ b/answerwc/to_client.go
@@ -0,0 +1,373 @@
+package answerwc
+
+import (
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "github.com/themakers/hlog"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/customer"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/mailclient"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/wctools"
+ "time"
+
+ "github.com/go-redis/redis/v8"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+)
+
+type DepsSendToClient struct {
+ Redis *redis.Client
+ Dal *dal.DAL
+ MailClient *mailclient.Client
+ CustomerService customer.CustomerServiceClient
+}
+
+type SendToClient struct {
+ deps DepsSendToClient
+ logger hlog.Logger
+ errChan chan<- error
+}
+
+type PendingTasks struct {
+ Count int64
+ QuizConfig model.QuizConfig
+}
+
+//go:embed mail/to_client.tmpl
+var toClientTemplate string
+
+//go:embed mail/reminder.tmpl
+var reminderTemplate string
+
+func NewSendToClient(deps DepsSendToClient, logger hlog.Logger, errChan chan<- error) *SendToClient {
+ return &SendToClient{
+ deps: deps,
+ logger: logger,
+ errChan: errChan,
+ }
+}
+
+func (w *SendToClient) Start(ctx context.Context) {
+ answerTicker := time.NewTicker(30 * time.Second)
+ defer answerTicker.Stop()
+
+ for {
+ select {
+ case <-answerTicker.C:
+ w.processPendingAnswer(ctx)
+
+ case <-ctx.Done():
+ w.logger.Module("To client worker terminated")
+ return
+
+ }
+ }
+}
+
+func (w *SendToClient) processPendingAnswer(ctx context.Context) {
+ pendingAnswers, err := w.deps.Redis.Keys(ctx, "answer:*").Result()
+ if err != nil {
+ fmt.Println("Error getting keys from redis")
+ w.errChan <- err
+ return
+ }
+
+ fmt.Println("ANS")
+
+ for _, key := range pendingAnswers {
+ func() {
+ fmt.Println("ANS1", key)
+ answerJSON, err := w.deps.Redis.GetDel(ctx, key).Result()
+ if err == redis.Nil {
+ return
+ } else if err != nil {
+ w.reportError(err, "Error getting and deleting data from redis")
+ return
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ w.reportError(nil, fmt.Sprintf("recovering from panic or error setting redis value %v", r))
+ fmt.Println("ANS1ERRR", r)
+ _ = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
+ }
+ }()
+
+ var answer model.Answer
+ err = json.Unmarshal([]byte(answerJSON), &answer)
+ fmt.Println("ANS2", err)
+ if err != nil {
+ w.reportError(err, "Error unmarshal answer")
+ return
+ }
+
+ answerContent, err := wctools.ProcessAnswer(answer.Content)
+ fmt.Println("ANS3", err)
+ if err != nil {
+ w.reportError(err, "Error unmarshal answer content")
+ return
+ }
+
+ allAnswers, err := w.deps.Dal.AnswerRepo.GetAllAnswersByQuizID(ctx, answer.Session)
+ fmt.Println("ANS4", err)
+ if err != nil {
+ w.reportError(err, "Error getting all answers by quizID")
+ return
+ }
+
+ questionsMap, err := w.deps.Dal.QuestionRepo.GetMapQuestions(ctx, allAnswers)
+ fmt.Println("ANS5", err)
+ if err != nil {
+ w.reportError(err, "Error getting questionsMap")
+ return
+ }
+
+ if answer.QuizId == 0 {
+ return
+ }
+
+ quizConfig, accountId, err := w.deps.Dal.QuizRepo.GetQuizConfig(ctx, answer.QuizId)
+ fmt.Println("ANS6", err)
+ if err != nil {
+ w.reportError(err, "Error getting quiz config")
+ return
+ }
+
+ quiz, err := w.deps.Dal.QuizRepo.GetQuizById(ctx, accountId, answer.QuizId)
+ fmt.Println("ANS60", err, accountId, answer.QuizId)
+ if err != nil {
+ w.reportError(err, "Error getting quiz")
+ return
+ }
+
+ quizConfig.Mailing.Reply = quiz.Name
+
+ if quizConfig.Mailing.Theme == "" {
+ quizConfig.Mailing.Theme = quiz.Name
+ }
+
+ account, privileges, err := w.deps.Dal.AccountRepo.GetAccAndPrivilegeByEmail(ctx, accountId)
+ fmt.Println("ANS7", err)
+ if err != nil {
+ w.reportError(err, "Error getting account and privileges by email")
+ return
+ }
+
+ result, err := w.processAnswerWithPrivileges(ctx, quiz.Name, quizConfig, questionsMap, privileges, account, allAnswers, answerContent, answer.CreatedAt)
+ fmt.Println("ANS8", err, result, privileges)
+ if err != nil {
+ w.reportError(err, "Error process answer with privileges")
+ return
+ }
+ if !result {
+ err = w.deps.Redis.Set(ctx, fmt.Sprintf("%s:%s", account.ID, key), answerJSON, 0).Err()
+ if err != nil {
+ w.reportError(err, "Error setting redis value")
+ return
+ }
+ }
+ }()
+ }
+}
+
+func (w *SendToClient) processAnswerWithPrivileges(ctx context.Context, quizName string, quizConfig model.QuizConfig,
+ questionsMap map[uint64]string, privileges []model.ShortPrivilege, account model.Account, allAnswers []model.ResultAnswer,
+ answerContent model.ResultContent, answerTime time.Time) (bool, error) {
+
+ err := w.notificationCustomer(account, privileges)
+ fmt.Println("ANS81", err)
+ if err != nil {
+ return false, err
+ }
+
+ if wctools.HasUnlimitedPrivilege(privileges) {
+ err := w.ProcessMessageToClient(quizConfig, questionsMap, account, allAnswers, answerContent, answerTime)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+ }
+ privilege := wctools.HasQuizCntPrivilege(privileges)
+ if privilege != nil {
+ err := w.ProcessMessageToClient(quizConfig, questionsMap, account, allAnswers, answerContent, answerTime)
+ fmt.Println("PMC", err)
+ if err != nil {
+ return true, err
+ }
+
+ privilege.Amount--
+ err = w.deps.Dal.AccountRepo.UpdatePrivilegeAmount(ctx, privilege.ID, privilege.Amount)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+ } else {
+ w.checkAndSendTaskReminders(ctx, sendTaskRemindersDeps{
+ email: account.Email,
+ theme: quizName,
+ config: model.QuizConfig{
+ Mailing: model.ResultInfo{
+ When: "email",
+ Theme: fmt.Sprintf("не удалось отправить заявку по опросу\"%s\"", quizName),
+ Reply: "noreply@pena.digital",
+ ReplName: "Reminder",
+ },
+ },
+ })
+ return false, nil
+ }
+}
+
+func (w *SendToClient) recordPendingTasks(ctx context.Context, Email string, quizConfig model.QuizConfig) error {
+ key := fmt.Sprintf("pending_tasks:%s", Email)
+
+ var pendingTasks PendingTasks
+ val, err := w.deps.Redis.HGet(ctx, key, "data").Result()
+ if err == nil {
+ err := json.Unmarshal([]byte(val), &pendingTasks)
+ if err != nil {
+ return err
+ }
+ pendingTasks.Count++
+ } else {
+ pendingTasks = PendingTasks{
+ Count: 1,
+ QuizConfig: quizConfig,
+ }
+ }
+
+ pendingTasksJSON, err := json.Marshal(pendingTasks)
+ if err != nil {
+ return err
+ }
+
+ err = w.deps.Redis.HSet(ctx, key, "data", string(pendingTasksJSON)).Err()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type sendTaskRemindersDeps struct {
+ email, theme string
+ config model.QuizConfig
+}
+
+func (w *SendToClient) checkAndSendTaskReminders(ctx context.Context, deps sendTaskRemindersDeps) {
+ err := w.processReminderToClient(deps.email, deps.config)
+ fmt.Println("PMC1", err)
+ if err != nil {
+ w.reportError(err, "Error sending tasks reminder email")
+ }
+}
+
+func (w *SendToClient) notificationCustomer(account model.Account, privileges []model.ShortPrivilege) error {
+ for _, privilege := range privileges {
+ fmt.Println("NOTIFIC", privilege.PrivilegeID, privilege.Amount, !wctools.IsPrivilegeExpired(privilege))
+ if privilege.PrivilegeID == "quizUnlimTime" && !wctools.IsPrivilegeExpired(privilege) {
+ rawDetail, err := wctools.ToJSON(privilege)
+ historyData := &customer.History{
+ UserID: account.UserID,
+ Comment: fmt.Sprintf("Привилегия %s просрочена", privilege.PrivilegeID),
+ Key: "privilege_expired",
+ RawDetails: rawDetail,
+ }
+
+ _, err = w.deps.CustomerService.InsertHistory(context.Background(), historyData)
+ if err != nil {
+ return err
+ }
+
+ }
+
+ if privilege.PrivilegeID == "quizCnt" && privilege.Amount == 0 {
+ rawDetail, err := wctools.ToJSON(privilege)
+ if err != nil {
+ return err
+ }
+ historyData := &customer.History{
+ UserID: account.UserID,
+ Comment: fmt.Sprintf("У привилегии %s истек amount", privilege.PrivilegeID),
+ Key: "privilege_expired",
+ RawDetails: rawDetail,
+ }
+
+ _, err = w.deps.CustomerService.InsertHistory(context.Background(), historyData)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// сделал экспортируемым для теста
+func (w *SendToClient) ProcessMessageToClient(quizConfig model.QuizConfig, questionsMap map[uint64]string, account model.Account, allAnswers []model.ResultAnswer, answerContent model.ResultContent, answerTime time.Time) error {
+ theme := quizConfig.Mailing.Theme
+ quizConfig.Mailing.Theme = quizConfig.Mailing.Reply
+
+ data := mailclient.EmailTemplateData{
+ QuizConfig: quizConfig.Mailing,
+ AnswerContent: answerContent,
+ AllAnswers: allAnswers,
+ QuestionsMap: questionsMap,
+ }
+
+ dayOfWeek := wctools.DaysOfWeek[answerTime.Format("Monday")]
+ monthOfYear := wctools.MonthsOfYear[answerTime.Format("January")]
+
+ formattedTime := fmt.Sprintf("%s, %d %s %d г., %02d:%02d (UTC%s)",
+ dayOfWeek,
+ answerTime.Day(),
+ monthOfYear,
+ answerTime.Year(),
+ answerTime.Hour(),
+ answerTime.Minute(),
+ answerTime.Format("-07:00"),
+ )
+
+ data.AnswerTime = formattedTime
+
+ fmt.Println("SUBJECT", theme, account.Email)
+
+ err := w.deps.MailClient.SendMailWithAttachment(account.Email, theme, toClientTemplate, data, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (w *SendToClient) processReminderToClient(email string, quizConfig model.QuizConfig) error {
+
+ data := mailclient.EmailTemplateData{
+ QuizConfig: model.ResultInfo{
+ When: quizConfig.Mailing.When,
+ Theme: quizConfig.Mailing.Theme,
+ Reply: email,
+ ReplName: quizConfig.Mailing.ReplName,
+ },
+ AnswerContent: model.ResultContent{},
+ AllAnswers: []model.ResultAnswer{},
+ QuestionsMap: nil,
+ }
+
+ fmt.Println("PRTC", data, email, quizConfig)
+
+ err := w.deps.MailClient.SendMailWithAttachment(email, quizConfig.Mailing.Theme, reminderTemplate, data, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (w *SendToClient) reportError(err error, message string) {
+ if err != nil {
+ fmt.Println(message + ": " + err.Error())
+ w.errChan <- err
+ }
+}
diff --git a/app/app.go b/app/app.go
new file mode 100644
index 0000000..c8fb260
--- /dev/null
+++ b/app/app.go
@@ -0,0 +1,209 @@
+package app
+
+import (
+ "context"
+ "errors"
+ "github.com/go-redis/redis/v8"
+ "github.com/gofiber/fiber/v2"
+ "github.com/skeris/appInit"
+ "github.com/themakers/hlog"
+ "go.uber.org/zap"
+ "google.golang.org/grpc"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/answerwc"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/customer"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/mailclient"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/privilegewc"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers/shortstat"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers/timeout"
+ "time"
+)
+
+type App struct {
+ logger *zap.Logger
+ err chan error
+}
+
+func (a App) GetLogger() *zap.Logger {
+ return a.logger
+}
+
+func (a App) GetErr() chan error {
+ return a.err
+}
+
+var (
+ errInvalidOptions = errors.New("invalid options")
+)
+
+var zapOptions = []zap.Option{
+ zap.AddCaller(),
+ zap.AddCallerSkip(2),
+ zap.AddStacktrace(zap.ErrorLevel),
+}
+
+var _ appInit.CommonApp = (*App)(nil)
+
+type Options struct {
+ ServiceName string `env:"SERVICE_NAME" default:"squiz"`
+ KafkaBroker string `env:"KAFKA_BROKER"`
+ KafkaTopic string `env:"KAFKA_TOPIC"`
+ PrivilegeID string `env:"QUIZ_ID"`
+ Amount uint64 `env:"AMOUNT"`
+ UnlimID string `env:"UNLIM_ID"`
+ LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
+ IsProd bool `env:"IS_PROD" default:"false"`
+ PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
+ RedisHost string `env:"REDIS_HOST"`
+ RedisPassword string `env:"REDIS_PASSWORD"`
+ RedisDB uint64 `env:"REDIS_DB"`
+ SmtpHost string `env:"SMTP_HOST"`
+ SmtpPort string `env:"SMTP_PORT"`
+ SmtpSender string `env:"SMTP_SENDER"`
+ SmtpIdentity string `env:"SMTP_IDENTITY"`
+ SmtpUsername string `env:"SMTP_USERNAME"`
+ SmtpPassword string `env:"SMTP_PASSWORD"`
+ SmtpApiKey string `env:"SMTP_API_KEY"`
+ CustomerServiceAddress string `env:"CUSTOMER_SERVICE_ADDRESS"`
+}
+
+func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
+ var (
+ err, workerErr error
+ zapLogger *zap.Logger
+ errChan = make(chan error)
+ options Options
+ ok bool
+ )
+
+ if options, ok = opts.(Options); !ok {
+ return App{}, errInvalidOptions
+ }
+
+ if options.LoggerProdMode {
+ zapLogger, err = zap.NewProduction(zapOptions...)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ zapLogger, err = zap.NewDevelopment(zapOptions...)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ zapLogger = zapLogger.With(
+ zap.String("SvcCommit", ver.Commit),
+ zap.String("SvcVersion", ver.Release),
+ zap.String("SvcBuildTime", ver.BuildTime),
+ )
+
+ logger := hlog.New(zapLogger)
+ logger.Emit(InfoSvcStarted{})
+ zapLogger.Info("config", zap.Any("options", options))
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case err := <-errChan:
+ zapLogger.Error("Ошибка при работе воркера", zap.Error(err))
+ }
+ }
+ }()
+
+ //init redis
+ redisClient := redis.NewClient(&redis.Options{
+ Addr: options.RedisHost,
+ Password: options.RedisPassword,
+ DB: int(options.RedisDB),
+ })
+
+ smtpData := mailclient.ClientDeps{
+ Host: options.SmtpHost,
+ Port: options.SmtpPort,
+ Sender: options.SmtpSender,
+ ApiKey: options.SmtpApiKey,
+ Auth: &mailclient.PlainAuth{
+ Identity: options.SmtpIdentity,
+ Username: options.SmtpUsername,
+ Password: options.SmtpPassword,
+ },
+ FiberClient: &fiber.Client{},
+ Logger: logger,
+ }
+
+ mailClient := mailclient.NewClient(smtpData)
+
+ customerServiceConn, err := grpc.Dial(options.CustomerServiceAddress, grpc.WithInsecure())
+ if err != nil {
+ return nil, err
+ }
+ customerServiceClient := customer.NewCustomerServiceClient(customerServiceConn)
+
+ pgdal, err := dal.New(ctx, options.PostgresCredentials, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ kafkaWorker, err := privilegewc.NewKafkaConsumerWorker(privilegewc.Config{
+ KafkaBroker: options.KafkaBroker,
+ KafkaTopic: options.KafkaTopic,
+ ServiceKey: options.ServiceName,
+ TickerInterval: time.Second * 10,
+ Logger: logger,
+ ErrChan: errChan,
+ }, redisClient, pgdal)
+ if err != nil {
+ logger.Module("Failed start privilege worker")
+ return nil, err
+ }
+
+ checkWorker := privilegewc.NewCheckWorker(privilegewc.CheckWorkerConfig{
+ DefaultData: model.DefaultData{
+ PrivilegeID: options.PrivilegeID,
+ Amount: options.Amount,
+ UnlimID: options.UnlimID,
+ },
+ TickerInterval: time.Minute,
+ Logger: logger,
+ ErrChan: errChan,
+ }, pgdal)
+
+ go kafkaWorker.Start(ctx)
+ go checkWorker.Start(ctx)
+ toClientWorker := answerwc.NewSendToClient(answerwc.DepsSendToClient{
+ Redis: redisClient,
+ Dal: pgdal,
+ MailClient: mailClient,
+ CustomerService: customerServiceClient,
+ }, logger, errChan)
+
+ toRespWorker := answerwc.NewRespWorker(answerwc.DepsRespWorker{
+ Redis: redisClient,
+ Dal: pgdal,
+ MailClient: mailClient,
+ }, logger, errChan)
+
+ go toClientWorker.Start(ctx)
+ go toRespWorker.Start(ctx)
+
+ tow := timeout.New(pgdal, time.Minute)
+ statW := shortstat.New(pgdal, 5*time.Minute)
+ tow.ExposeErr(ctx, &workerErr)
+ statW.ExposeErr(ctx, &workerErr)
+ go tow.Start(ctx)
+ go func() {
+ // defer pgdal.CloseWorker()
+ statW.Start(ctx)
+ }()
+
+ logger.Emit(InfoSvcReady{})
+ // todo implement helper func for service app type. such as server preparing, logger preparing, healthchecks and etc.
+ return &App{
+ logger: zapLogger,
+ err: make(chan error),
+ }, err
+}
diff --git a/app/logrecords.go b/app/logrecords.go
new file mode 100644
index 0000000..0dbaf96
--- /dev/null
+++ b/app/logrecords.go
@@ -0,0 +1,10 @@
+package app
+
+type InfoSvcStarted struct{}
+type InfoSvcReady struct{}
+type InfoSvcShutdown struct {
+ Signal string
+}
+type ErrorCanNotServe struct {
+ Err error
+}
diff --git a/clients/customer/service.pb.go b/clients/customer/service.pb.go
new file mode 100644
index 0000000..510635d
--- /dev/null
+++ b/clients/customer/service.pb.go
@@ -0,0 +1,182 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.31.0
+// protoc (unknown)
+// source: customer/service.proto
+
+package customer
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ emptypb "google.golang.org/protobuf/types/known/emptypb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type History struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ UserID string `protobuf:"bytes,1,opt,name=UserID,proto3" json:"UserID,omitempty"`
+ Comment string `protobuf:"bytes,2,opt,name=Comment,proto3" json:"Comment,omitempty"`
+ Key string `protobuf:"bytes,3,opt,name=Key,proto3" json:"Key,omitempty"`
+ RawDetails string `protobuf:"bytes,4,opt,name=RawDetails,proto3" json:"RawDetails,omitempty"`
+}
+
+func (x *History) Reset() {
+ *x = History{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_customer_service_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *History) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*History) ProtoMessage() {}
+
+func (x *History) ProtoReflect() protoreflect.Message {
+ mi := &file_customer_service_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use History.ProtoReflect.Descriptor instead.
+func (*History) Descriptor() ([]byte, []int) {
+ return file_customer_service_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *History) GetUserID() string {
+ if x != nil {
+ return x.UserID
+ }
+ return ""
+}
+
+func (x *History) GetComment() string {
+ if x != nil {
+ return x.Comment
+ }
+ return ""
+}
+
+func (x *History) GetKey() string {
+ if x != nil {
+ return x.Key
+ }
+ return ""
+}
+
+func (x *History) GetRawDetails() string {
+ if x != nil {
+ return x.RawDetails
+ }
+ return ""
+}
+
+var File_customer_service_proto protoreflect.FileDescriptor
+
+var file_customer_service_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+ 0x65, 0x72, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
+ 0x6d, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x73,
+ 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x55, 0x73, 0x65, 0x72,
+ 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03,
+ 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x1e,
+ 0x0a, 0x0a, 0x52, 0x61, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0a, 0x52, 0x61, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x32, 0x4f,
+ 0x0a, 0x0f, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x48, 0x69, 0x73, 0x74, 0x6f,
+ 0x72, 0x79, 0x12, 0x11, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x65, 0x72, 0x2e, 0x48, 0x69,
+ 0x73, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42,
+ 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x65, 0x72, 0x62, 0x06, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_customer_service_proto_rawDescOnce sync.Once
+ file_customer_service_proto_rawDescData = file_customer_service_proto_rawDesc
+)
+
+func file_customer_service_proto_rawDescGZIP() []byte {
+ file_customer_service_proto_rawDescOnce.Do(func() {
+ file_customer_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_customer_service_proto_rawDescData)
+ })
+ return file_customer_service_proto_rawDescData
+}
+
+var file_customer_service_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_customer_service_proto_goTypes = []interface{}{
+ (*History)(nil), // 0: customer.History
+ (*emptypb.Empty)(nil), // 1: google.protobuf.Empty
+}
+var file_customer_service_proto_depIdxs = []int32{
+ 0, // 0: customer.CustomerService.InsertHistory:input_type -> customer.History
+ 1, // 1: customer.CustomerService.InsertHistory:output_type -> google.protobuf.Empty
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_customer_service_proto_init() }
+func file_customer_service_proto_init() {
+ if File_customer_service_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_customer_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*History); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_customer_service_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_customer_service_proto_goTypes,
+ DependencyIndexes: file_customer_service_proto_depIdxs,
+ MessageInfos: file_customer_service_proto_msgTypes,
+ }.Build()
+ File_customer_service_proto = out.File
+ file_customer_service_proto_rawDesc = nil
+ file_customer_service_proto_goTypes = nil
+ file_customer_service_proto_depIdxs = nil
+}
diff --git a/clients/customer/service_grpc.pb.go b/clients/customer/service_grpc.pb.go
new file mode 100644
index 0000000..74454d3
--- /dev/null
+++ b/clients/customer/service_grpc.pb.go
@@ -0,0 +1,108 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: customer/service.proto
+
+package customer
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ emptypb "google.golang.org/protobuf/types/known/emptypb"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ CustomerService_InsertHistory_FullMethodName = "/customer.CustomerService/InsertHistory"
+)
+
+// CustomerServiceClient is the client API for CustomerService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CustomerServiceClient interface {
+ InsertHistory(ctx context.Context, in *History, opts ...grpc.CallOption) (*emptypb.Empty, error)
+}
+
+type customerServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewCustomerServiceClient(cc grpc.ClientConnInterface) CustomerServiceClient {
+ return &customerServiceClient{cc}
+}
+
+func (c *customerServiceClient) InsertHistory(ctx context.Context, in *History, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+ out := new(emptypb.Empty)
+ err := c.cc.Invoke(ctx, CustomerService_InsertHistory_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// CustomerServiceServer is the server API for CustomerService service.
+// All implementations should embed UnimplementedCustomerServiceServer
+// for forward compatibility
+type CustomerServiceServer interface {
+ InsertHistory(context.Context, *History) (*emptypb.Empty, error)
+}
+
+// UnimplementedCustomerServiceServer should be embedded to have forward compatible implementations.
+type UnimplementedCustomerServiceServer struct {
+}
+
+func (UnimplementedCustomerServiceServer) InsertHistory(context.Context, *History) (*emptypb.Empty, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method InsertHistory not implemented")
+}
+
+// UnsafeCustomerServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CustomerServiceServer will
+// result in compilation errors.
+type UnsafeCustomerServiceServer interface {
+ mustEmbedUnimplementedCustomerServiceServer()
+}
+
+func RegisterCustomerServiceServer(s grpc.ServiceRegistrar, srv CustomerServiceServer) {
+ s.RegisterService(&CustomerService_ServiceDesc, srv)
+}
+
+func _CustomerService_InsertHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(History)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CustomerServiceServer).InsertHistory(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CustomerService_InsertHistory_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CustomerServiceServer).InsertHistory(ctx, req.(*History))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// CustomerService_ServiceDesc is the grpc.ServiceDesc for CustomerService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CustomerService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "customer.CustomerService",
+ HandlerType: (*CustomerServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "InsertHistory",
+ Handler: _CustomerService_InsertHistory_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "customer/service.proto",
+}
diff --git a/clients/mailclient/client.go b/clients/mailclient/client.go
new file mode 100644
index 0000000..fefb360
--- /dev/null
+++ b/clients/mailclient/client.go
@@ -0,0 +1,144 @@
+package mailclient
+
+import (
+ "bytes"
+ _ "embed"
+ "fmt"
+ "github.com/gofiber/fiber/v2"
+ "github.com/themakers/hlog"
+ "mime/multipart"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "strings"
+ "text/template"
+)
+
+type ClientDeps struct {
+ Host string
+ Port string
+ Sender string
+ Auth *PlainAuth
+ ApiKey string
+ FiberClient *fiber.Client
+ Logger hlog.Logger
+}
+
+type Client struct {
+ deps ClientDeps
+}
+
+type PlainAuth struct {
+ Identity string
+ Username string
+ Password string
+}
+
+type EmailTemplateData struct {
+ QuizConfig model.ResultInfo
+ AnswerContent model.ResultContent
+ AllAnswers []model.ResultAnswer
+ QuestionsMap map[uint64]string
+ AnswerTime string
+}
+
+func NewClient(deps ClientDeps) *Client {
+ if deps.FiberClient == nil {
+ deps.FiberClient = fiber.AcquireClient()
+ }
+ return &Client{
+ deps: deps,
+ }
+}
+
+func (c *Client) SendMailWithAttachment(recipient, subject string, emailTemplate string, data EmailTemplateData, attachments []Attachment) error {
+ text, err := generateTextFromTemplate(data, emailTemplate)
+ if err != nil {
+ c.deps.Logger.Module("Error generate text from template")
+ return err
+ }
+
+ msg := &Message{
+ From: c.deps.Sender,
+ To: []string{recipient},
+ Subject: subject,
+ Body: text,
+ Attachments: attachments,
+ }
+
+ return c.Send(msg)
+}
+
+func (c *Client) Send(msg *Message) error {
+ url := "https://api.smtp.bz/v1/smtp/send"
+
+ form := new(bytes.Buffer)
+ writer := multipart.NewWriter(form)
+ defer writer.Close()
+
+ fields := map[string]string{
+ "from": msg.From,
+ "to": strings.Join(msg.To, ","),
+ "subject": msg.Subject,
+ "html": msg.Body,
+ }
+ for key, value := range fields {
+ if err := writer.WriteField(key, value); err != nil {
+ c.deps.Logger.Module("Error creating form fields")
+ return err
+ }
+ }
+
+ for _, attachment := range msg.Attachments {
+ part, err := writer.CreateFormFile("attachments", attachment.Name)
+ if err != nil {
+ c.deps.Logger.Module("Error creating form file for attachment")
+ return err
+ }
+ _, err = part.Write(attachment.Data)
+ if err != nil {
+ c.deps.Logger.Module("Error writing attachment content")
+ return err
+ }
+ }
+
+ if err := writer.Close(); err != nil {
+ c.deps.Logger.Module("Error closing writer for multipart form")
+ return err
+ }
+
+ agent := c.deps.FiberClient.Post(url).Body(form.Bytes()).ContentType(writer.FormDataContentType())
+ if c.deps.ApiKey != "" {
+ agent.Set("Authorization", c.deps.ApiKey)
+ }
+
+ statusCode, body, errs := agent.Bytes()
+ if errs != nil {
+ c.deps.Logger.Module("Error sending request")
+ return errs[0]
+ }
+
+ if statusCode != fiber.StatusOK {
+ return fmt.Errorf("SMTP service returned error: %d, Response body: %s", statusCode, body)
+ }
+
+ return nil
+}
+
+func generateTextFromTemplate(data EmailTemplateData, tpl string) (string, error) {
+ t, err := template.New("email").Parse(tpl)
+ if err != nil {
+ return "", fmt.Errorf("error parsing template: %w", err)
+ }
+
+ var text bytes.Buffer
+ if err := t.Execute(&text, EmailTemplateData{
+ QuizConfig: data.QuizConfig,
+ AnswerContent: data.AnswerContent,
+ AllAnswers: data.AllAnswers,
+ QuestionsMap: data.QuestionsMap,
+ AnswerTime: data.AnswerTime,
+ }); err != nil {
+ return "", fmt.Errorf("error executing template: %w", err)
+ }
+
+ return text.String(), nil
+}
diff --git a/clients/mailclient/message.go b/clients/mailclient/message.go
new file mode 100644
index 0000000..06261ca
--- /dev/null
+++ b/clients/mailclient/message.go
@@ -0,0 +1,104 @@
+package mailclient
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type Message struct {
+ To []string
+ From string
+ Subject string
+ Body string
+ Attachments []Attachment
+}
+
+type Attachment struct {
+ Name string
+ Data []byte
+}
+
+func NewMessage(subject, body string) *Message {
+ if subject == "" {
+ subject = "Вам пришла заявка с PenaQuiz"
+ }
+ return &Message{Subject: subject, Body: body, Attachments: []Attachment{}}
+}
+
+func (m *Message) AttachFile(src string) error {
+ data, err := os.ReadFile(src)
+ if err != nil {
+ return err
+ }
+
+ _, filename := filepath.Split(src)
+ m.Attachments = append(m.Attachments, Attachment{Name: filename, Data: data})
+
+ return nil
+}
+
+func (m *Message) AttachBytesFile(filename string, data []byte) {
+ m.Attachments = append(m.Attachments, Attachment{Name: filename, Data: data})
+}
+
+func (m *Message) ToBytes() []byte {
+ buf := bytes.NewBuffer(nil)
+
+ buf.WriteString("MIME-Version: 1.0\r\n")
+ fmt.Fprintf(buf, "From: %s\r\n", m.From)
+ fmt.Fprintf(buf, "Subject: %s\r\n", m.Subject)
+ fmt.Fprintf(buf, "To: %s\r\n", strings.Join(m.To, ","))
+
+ boundary := randomBoundary()
+
+ if len(m.Attachments) > 0 {
+ buf.WriteString("Content-Type: multipart/mixed;\r\n")
+
+ fmt.Fprintf(buf, " boundary=\"%s\"\r\n", boundary)
+
+ fmt.Fprintf(buf, "\r\n--%s", boundary)
+ for _, attachment := range m.Attachments {
+ buf.WriteString("\r\n")
+ switch strings.Split(attachment.Name, ".")[1] {
+ case "htmlmsg":
+
+ buf.WriteString("Content-Type: text/html; charset=\"utf-8\"\r\n")
+ buf.WriteString("Content-Transfer-Encoding: base64\r\n")
+ case "docx":
+
+ buf.WriteString("Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n")
+ buf.WriteString("Content-Transfer-Encoding: base64\r\n")
+ fmt.Fprintf(buf, "Content-Disposition: attachment; filename=\"%s\"\r\n", attachment.Name)
+ default:
+ fmt.Fprintf(buf, "Content-Type: %s\r\n", http.DetectContentType(attachment.Data))
+ buf.WriteString("Content-Transfer-Encoding: base64\r\n")
+ fmt.Fprintf(buf, "Content-Disposition: attachment; filename=\"%s\"\r\n", attachment.Name)
+ }
+
+ buf.WriteString("\r\n")
+
+ b := make([]byte, base64.StdEncoding.EncodedLen(len(attachment.Data)))
+ base64.StdEncoding.Encode(b, attachment.Data)
+
+ writer := NewLineWriter(buf, 76)
+ _, err := writer.Write(b)
+ if err != nil {
+ fmt.Println("mailclient-client err:", err)
+ }
+
+ fmt.Fprintf(buf, "\r\n\r\n--%s", boundary)
+ }
+
+ buf.WriteString("--")
+ } else {
+ buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
+ buf.WriteString(m.Body)
+ }
+
+ return buf.Bytes()
+}
diff --git a/clients/mailclient/utils.go b/clients/mailclient/utils.go
new file mode 100644
index 0000000..1f01408
--- /dev/null
+++ b/clients/mailclient/utils.go
@@ -0,0 +1,63 @@
+package mailclient
+
+import (
+ "crypto/rand"
+ "fmt"
+ "io"
+)
+
+type LineWriter struct {
+ w io.Writer
+ length int
+}
+
+func NewLineWriter(w io.Writer, length int) *LineWriter {
+ return &LineWriter{
+ w: w,
+ length: length,
+ }
+}
+
+func (r *LineWriter) Write(p []byte) (n int, err error) {
+ for i := 0; i < len(p); i += r.length {
+ end := i + r.length
+
+ if end > len(p) {
+ end = len(p) - 1
+ }
+
+ var chunk []byte
+ chunk = append(chunk, p[i:end]...)
+
+ if len(p) >= end+r.length {
+ chunk = append(chunk, []byte("\r\n")...)
+ }
+
+ addN, err := r.w.Write(chunk)
+ if err != nil {
+ return n, err
+ }
+ n += addN
+ }
+
+ return n, nil
+}
+
+func (r *LineWriter) WriteString(s string) (n int, err error) {
+ p := []byte(s)
+ return r.Write(p)
+}
+
+func (r *LineWriter) WriteFormatString(format string, a ...any) (n int, err error) {
+ p := []byte(fmt.Sprintf(format, a...))
+ return r.Write(p)
+}
+
+func randomBoundary() string {
+ var buf [30]byte
+ _, err := io.ReadFull(rand.Reader, buf[:])
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("%x", buf[:])
+}
diff --git a/deployments/local/docker-compose.yaml b/deployments/local/docker-compose.yaml
new file mode 100644
index 0000000..2f80b77
--- /dev/null
+++ b/deployments/local/docker-compose.yaml
@@ -0,0 +1,12 @@
+services:
+ postgres:
+ image: postgres
+ restart: always
+ environment:
+ POSTGRES_PASSWORD: Redalert2
+ POSTGRES_USER: squiz
+ POSTGRES_DB: squiz
+ app:
+ image: penahub.gitlab.yandexcloud.net:5050/backend/squiz:latest
+ ports:
+ - 1488:1488
diff --git a/deployments/main/docker-compose.yaml b/deployments/main/docker-compose.yaml
new file mode 100644
index 0000000..da5ff20
--- /dev/null
+++ b/deployments/main/docker-compose.yaml
@@ -0,0 +1,77 @@
+services:
+ core:
+ hostname: squiz-core
+ container_name: squiz-core
+ image: $CI_REGISTRY_IMAGE/main-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ HUB_ADMIN_URL: 'http://10.8.0.8:59303'
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PORT: 1488
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ AUTH_URL: 'http://10.8.0.8:59300/user'
+ ports:
+ - 10.8.0.9:1488:1488
+
+ storer:
+ hostname: squiz-storer
+ container_name: squiz-storer
+ image: $CI_REGISTRY_IMAGE/main-storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1489
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ ports:
+ - 10.8.0.9:1489:1489
+ worker:
+ hostname: squiz-worker
+ container_name: squiz-worker
+ image: $CI_REGISTRY_IMAGE/main-worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ KAFKA_BROKER: '10.8.0.8:9092'
+ KAFKA_TOPIC: 'tariffs'
+ QUIZ_ID: quizCnt
+ AMOUNT: 10
+ UNLIM_ID: quizUnlimTime
+ REDIS_HOST: '10.8.0.9:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ SMTP_API_URL: 'https://api.smtp.bz/v1/smtp/send'
+ SMTP_HOST: 'connect.smtp.bz'
+ SMTP_PORT: '587'
+ SMTP_UNAME: 'team@pena.digital'
+ SMTP_PASS: 'AyMfwqA9LkQH'
+ SMTP_API_KEY: '8tv2xcsfCMBX3TCQxzgeeEwAEYyQrPUp0ggw'
+ SMTP_SENDER: 'recovery@noreply.pena.digital'
+ CUSTOMER_SERVICE_ADDRESS: 'http://10.8.0.8:8065/'
+ answerer:
+ hostname: squiz-answerer
+ container_name: squiz-answerer
+ image: $CI_REGISTRY_IMAGE/main-answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1490
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ REDIS_HOST: '10.8.0.9:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ ports:
+ - 10.8.0.9:1490:1490
diff --git a/deployments/main/staging/docker-compose.yaml b/deployments/main/staging/docker-compose.yaml
new file mode 100644
index 0000000..4aaed26
--- /dev/null
+++ b/deployments/main/staging/docker-compose.yaml
@@ -0,0 +1,77 @@
+services:
+ core:
+ hostname: squiz-core
+ container_name: squiz-core
+ image: $CI_REGISTRY_IMAGE/core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ HUB_ADMIN_URL: 'http://10.6.0.11:59303'
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PORT: 1488
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ AUTH_URL: 'http://10.6.0.11:59300/user'
+ ports:
+ - 1488:1488
+
+ storer:
+ hostname: squiz-storer
+ container_name: squiz-storer
+ image: $CI_REGISTRY_IMAGE/storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1489
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ ports:
+ - 1489:1489
+ worker:
+ hostname: squiz-worker
+ container_name: squiz-worker
+ image: $CI_REGISTRY_IMAGE/worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ KAFKA_BROKER: '10.6.0.11:9092'
+ KAFKA_TOPIC: 'tariffs'
+ QUIZ_ID: quizCnt
+ AMOUNT: 10
+ UNLIM_ID: quizUnlimTime
+ REDIS_HOST: '10.6.0.23:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ SMTP_HOST: 'connect.mailclient.bz'
+ SMTP_PORT: '587'
+ SMTP_SENDER: 'noreply@mailing.pena.digital'
+ SMTP_IDENTITY: ''
+ SMTP_USERNAME: 'kotilion.95@gmail.com'
+ SMTP_PASSWORD: 'vWwbCSg4bf0p'
+ SMTP_API_KEY: 'P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev'
+ CUSTOMER_SERVICE_ADDRESS: 'http://10.6.0.11:8065/'
+ answerer:
+ hostname: squiz-answerer
+ container_name: squiz-answerer
+ image: $CI_REGISTRY_IMAGE/answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1490
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ REDIS_HOST: '10.6.0.23:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ ports:
+ - 1490:1490
diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml
new file mode 100644
index 0000000..76c10ae
--- /dev/null
+++ b/deployments/staging/docker-compose.yaml
@@ -0,0 +1,77 @@
+services:
+ core:
+ hostname: squiz-core
+ container_name: squiz-core
+ image: $CI_REGISTRY_IMAGE/staging-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ HUB_ADMIN_URL: 'http://10.6.0.11:59303'
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PORT: 1488
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ AUTH_URL: 'http://10.6.0.11:59300/user'
+ ports:
+ - 1488:1488
+
+ storer:
+ hostname: squiz-storer
+ container_name: squiz-storer
+ image: $CI_REGISTRY_IMAGE/staging-storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1489
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ ports:
+ - 1489:1489
+ worker:
+ hostname: squiz-worker
+ container_name: squiz-worker
+ image: $CI_REGISTRY_IMAGE/staging-worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ KAFKA_BROKER: '10.6.0.11:9092'
+ KAFKA_TOPIC: 'tariffs'
+ QUIZ_ID: quizCnt
+ AMOUNT: 10
+ UNLIM_ID: quizUnlimTime
+ REDIS_HOST: '10.6.0.23:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ SMTP_HOST: 'connect.mailclient.bz'
+ SMTP_PORT: '587'
+ SMTP_SENDER: 'noreply@mailing.pena.digital'
+ SMTP_IDENTITY: ''
+ SMTP_USERNAME: 'kotilion.95@gmail.com'
+ SMTP_PASSWORD: 'vWwbCSg4bf0p'
+ SMTP_API_KEY: 'P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev'
+ CUSTOMER_SERVICE_ADDRESS: 'http://10.6.0.11:8065/'
+ answerer:
+ hostname: squiz-answerer
+ container_name: squiz-answerer
+ image: $CI_REGISTRY_IMAGE/staging-answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
+ tty: true
+ environment:
+ IS_PROD_LOG: 'false'
+ IS_PROD: 'false'
+ PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
+ PORT: 1490
+ MINIO_EP: 'storage.yandexcloud.net'
+ MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
+ MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
+ PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
+ REDIS_HOST: '10.6.0.23:6379'
+ REDIS_PASSWORD: 'Redalert2'
+ REDIS_DB: 2
+ ports:
+ - 1490:1490
diff --git a/deployments/test/docker-compose.yaml b/deployments/test/docker-compose.yaml
new file mode 100644
index 0000000..b66713c
--- /dev/null
+++ b/deployments/test/docker-compose.yaml
@@ -0,0 +1,102 @@
+version: '3'
+services:
+ test-postgres:
+ image: postgres
+ environment:
+ POSTGRES_PASSWORD: Redalert2
+ POSTGRES_USER: squiz
+ POSTGRES_DB: squiz
+ volumes:
+ - test-postgres:/var/lib/postgresql/data
+ ports:
+ - 35432:5432
+ networks:
+ - penatest
+ healthcheck:
+ test: pg_isready -U squiz
+ interval: 2s
+ timeout: 2s
+ retries: 10
+
+# need update!
+# test-pena-auth-service:
+# image: penahub.gitlab.yandexcloud.net:5050/pena-services/pena-auth-service:staging.872
+# container_name: test-pena-auth-service
+# init: true
+# env_file: auth.env.test
+# healthcheck:
+# test: wget -T1 --spider http://localhost:8000/user
+# interval: 2s
+# timeout: 2s
+# retries: 5
+# environment:
+# - DB_HOST=test-pena-auth-db
+# - DB_PORT=27017
+# - ENVIRONMENT=staging
+# - HTTP_HOST=0.0.0.0
+# - HTTP_PORT=8000
+# - DB_USERNAME=test
+# - DB_PASSWORD=test
+# - DB_NAME=admin
+# - DB_AUTH=admin
+# # ports:
+# # - 8000:8000
+# depends_on:
+# - test-pena-auth-db
+# # - pena-auth-migration
+# networks:
+# - penatest
+#
+# test-pena-auth-db:
+# container_name: test-pena-auth-db
+# init: true
+# image: "mongo:6.0.3"
+# command: mongod --quiet --logpath /dev/null
+# volumes:
+# - test-mongodb:/data/db
+# - test-mongoconfdb:/data/configdb
+# environment:
+# MONGO_INITDB_ROOT_USERNAME: test
+# MONGO_INITDB_ROOT_PASSWORD: test
+# # ports:
+# # - 27017:27017
+# networks:
+# - penatest
+
+ test-minio:
+ container_name: test-minio
+ init: true
+ image: quay.io/minio/minio
+ volumes:
+ - test-minio:/data
+ command: [ "minio", "--quiet", "server", "/data" ]
+ networks:
+ - penatest
+
+ test-squiz:
+ container_name: test-squiz
+ init: true
+ build:
+ context: ../..
+ dockerfile: TestsDockerfile
+ depends_on:
+ test-postgres:
+ condition: service_healthy
+# test-pena-auth-service:
+# condition: service_healthy
+ # volumes:
+ # - ./../..:/app:ro
+ # command: [ "go", "test", "./tests", "-run", "TestFoo" ]
+ command: [ "go", "test", "-parallel", "1", "./tests" ]
+ networks:
+ - penatest
+
+networks:
+ penatest:
+
+
+volumes:
+ test-minio:
+ test-postgres:
+ test-mongodb:
+ test-mongoconfdb:
diff --git a/deployments/testmigrate/docker-compose.yaml b/deployments/testmigrate/docker-compose.yaml
new file mode 100644
index 0000000..dd98264
--- /dev/null
+++ b/deployments/testmigrate/docker-compose.yaml
@@ -0,0 +1,23 @@
+version: '3'
+services:
+ test-postgres:
+ image: postgres
+ environment:
+ POSTGRES_PASSWORD: Redalert2
+ POSTGRES_USER: squiz
+ POSTGRES_DB: squiz
+ ports:
+ - 35432:5432
+ networks:
+ - penatest
+ healthcheck:
+ test: pg_isready -U squiz
+ interval: 2s
+ timeout: 2s
+ retries: 10
+
+networks:
+ penatest:
+
+# просто чтоб тестануть мигрировала ли бд
+# в app/app.go pgdal, err := dal.New(ctx, "host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable")
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a5c9f32
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,45 @@
+module penahub.gitlab.yandexcloud.net/backend/quiz/worker.git
+
+go 1.21.4
+
+require (
+ github.com/go-redis/redis/v8 v8.11.5
+ github.com/gofiber/fiber/v2 v2.52.0
+ github.com/golang/protobuf v1.5.3
+ github.com/skeris/appInit v1.0.2
+ github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
+ github.com/twmb/franz-go v1.16.1
+ go.uber.org/zap v1.26.0
+ google.golang.org/grpc v1.61.1
+ google.golang.org/protobuf v1.32.0
+ penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219175507-7f8de986a6dc
+)
+
+require (
+ github.com/andybalholm/brotli v1.0.5 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/golang-migrate/migrate/v4 v4.17.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/klauspost/compress v1.17.4 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/pierrec/lz4/v4 v4.1.19 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/twmb/franz-go/pkg/kmsg v1.7.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.51.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ go.uber.org/atomic v1.7.0 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/net v0.18.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
+ penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d // indirect
+ penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..0fec870
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,174 @@
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M=
+github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78=
+github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
+github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
+github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
+github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
+github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
+github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/skeris/appInit v1.0.2 h1:Hr4KbXYd6kolTVq4cXGqDpgnpmaauiOiKizA1+Ep4KQ=
+github.com/skeris/appInit v1.0.2/go.mod h1:4ElEeXWVGzU3dlYq/eMWJ/U5hd+LKisc1z3+ySh1XmY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf h1:TJJm6KcBssmbWzplF5lzixXl1RBAi/ViPs1GaSOkhwo=
+github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf/go.mod h1:1FsorU3vnXO9xS9SrhUp8fRb/6H/Zfll0rPt1i4GWaA=
+github.com/twmb/franz-go v1.16.1 h1:rpWc7fB9jd7TgmCyfxzenBI+QbgS8ZfJOUQE+tzPtbE=
+github.com/twmb/franz-go v1.16.1/go.mod h1:/pER254UPPGp/4WfGqRi+SIRGE50RSQzVubQp6+N4FA=
+github.com/twmb/franz-go/pkg/kmsg v1.7.0 h1:a457IbvezYfA5UkiBvyV3zj0Is3y1i8EJgqjJYoij2E=
+github.com/twmb/franz-go/pkg/kmsg v1.7.0/go.mod h1:se9Mjdt0Nwzc9lnjJ0HyDtLyBnaBDAd7pCje47OhSyw=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
+github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
+golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
+golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
+google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
+google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d h1:gbaDt35HMDqOK84WYmDIlXMI7rstUcRqNttaT6Kx1do=
+penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=
+penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219175507-7f8de986a6dc h1:jIN9XyfL/FJ/eSsYopE1olHboituwmisC1Sf1d4nhWE=
+penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219175507-7f8de986a6dc/go.mod h1:OXYvMlc+3qfcllPTywUB3QDiPK1kwsMNdZMTlPXFIdo=
+penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af h1:jQ7HaXSutDX5iepU7VRImxhikK7lV/lBKkiloOZ4Emo=
+penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af/go.mod h1:5S5YwjSXWmnEKjBjG6MtyGtFmljjukDRS8CwHk/CF/I=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..6aee4be
--- /dev/null
+++ b/main.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+ "github.com/skeris/appInit"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/app"
+)
+
+func main() {
+ appInit.Initialize(app.New, app.Options{})
+}
diff --git a/privilegewc/check.go b/privilegewc/check.go
new file mode 100644
index 0000000..c2fd482
--- /dev/null
+++ b/privilegewc/check.go
@@ -0,0 +1,67 @@
+package privilegewc
+
+import (
+ "context"
+ "fmt"
+ "github.com/themakers/hlog"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "time"
+)
+
+type CheckWorkerConfig struct {
+ TickerInterval time.Duration
+ DefaultData model.DefaultData
+ Logger hlog.Logger
+ ErrChan chan<- error
+}
+
+type CheckWorker struct {
+ config CheckWorkerConfig
+ privilegeDAL *dal.DAL
+}
+
+func NewCheckWorker(config CheckWorkerConfig, privilegeDAL *dal.DAL) *CheckWorker {
+ return &CheckWorker{
+ config: config,
+ privilegeDAL: privilegeDAL,
+ }
+}
+
+func (w *CheckWorker) Start(ctx context.Context) {
+ ticker := time.NewTicker(w.config.TickerInterval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ fmt.Println("CHECK")
+ w.performScheduledTasks(ctx)
+ case <-ctx.Done():
+ fmt.Println("Check worker terminated")
+ return
+ }
+ }
+}
+
+// TODO: Maybe one query?
+func (w *CheckWorker) performScheduledTasks(ctx context.Context) {
+ fmt.Println("CHEC0")
+ w.deleteExpired(ctx)
+}
+
+func (w *CheckWorker) deleteExpired(ctx context.Context) {
+ expiredData, err := w.privilegeDAL.AccountRepo.GetExpired(ctx, w.config.DefaultData.UnlimID)
+ if err != nil {
+ w.config.Logger.Module("Error getting expired quizUnlimTime records")
+ w.config.ErrChan <- err
+ }
+
+ for _, data := range expiredData {
+ err := w.privilegeDAL.AccountRepo.DeletePrivilegeByID(ctx, data.ID)
+ if err != nil {
+ w.config.Logger.Module("Error deleting expired quizUnlimTime record")
+ w.config.ErrChan <- err
+ }
+ }
+}
diff --git a/privilegewc/consumer.go b/privilegewc/consumer.go
new file mode 100644
index 0000000..06d3188
--- /dev/null
+++ b/privilegewc/consumer.go
@@ -0,0 +1,155 @@
+package privilegewc
+
+import (
+ "context"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/themakers/hlog"
+ "github.com/twmb/franz-go/pkg/kgo"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/wctools"
+ "strings"
+ "time"
+)
+
+// Config содержит параметры конфигурации.
+type Config struct {
+ KafkaBroker string
+ KafkaTopic string
+ ServiceKey string
+ TickerInterval time.Duration
+ Logger hlog.Logger
+ ErrChan chan<- error
+}
+
+type KafkaConsumerWorker struct {
+ config Config
+ client *kgo.Client
+ redis *redis.Client
+ privilegeDAL *dal.DAL
+}
+
+// NewKafkaConsumerWorker создает новый экземпляр KafkaConsumerWorker.
+func NewKafkaConsumerWorker(config Config, redis *redis.Client, privilegeDAL *dal.DAL) (*KafkaConsumerWorker, error) {
+ client, err := kgo.NewClient(
+ kgo.SeedBrokers(config.KafkaBroker),
+ kgo.ConsumerGroup("squiz1"),
+ kgo.ConsumeTopics(config.KafkaTopic),
+ kgo.ConsumeResetOffset(kgo.NewOffset().AfterMilli(time.Now().UnixMilli())),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return &KafkaConsumerWorker{
+ config: config,
+ client: client,
+ privilegeDAL: privilegeDAL,
+ redis: redis,
+ }, nil
+}
+
+// Start запускает.
+func (w *KafkaConsumerWorker) Start(ctx context.Context) {
+ ticker := time.NewTicker(w.config.TickerInterval)
+ defer ticker.Stop()
+
+ for {
+ fmt.Println("KONSUMER", w.config.TickerInterval)
+ select {
+ case <-ticker.C:
+ w.fetchMessages(ctx)
+ case <-ctx.Done():
+ w.config.Logger.Module("Kafka worker terminated")
+ return
+ }
+ }
+}
+
+// fetchAndProcessMessages извлекает сообщения из темы Kafka и обрабатывает их.
+func (w *KafkaConsumerWorker) fetchMessages(ctx context.Context) {
+ fetches := w.client.PollFetches(ctx)
+ iter := fetches.RecordIter()
+
+ fmt.Println("KONSUMER1", fetches, w.config.ServiceKey)
+ for !iter.Done() {
+ record := iter.Next()
+
+ privilege, userID, err := wctools.IsValidMessage(record.Value, w.config.ServiceKey)
+ fmt.Println("KONSUMER2", err, userID)
+ if err != nil {
+ w.config.Logger.Module("Error validating Kafka message")
+ }
+
+ err = w.processValidMessage(ctx, privilege, userID)
+ if err != nil {
+ w.config.Logger.Module("Error processing valid message")
+ }
+ }
+}
+
+// processValidMessage обрабатывает валидное сообщение.
+func (w *KafkaConsumerWorker) processValidMessage(ctx context.Context, privilege []model.PrivilegeMessage, userID string) error {
+ currentPrivileges, err := w.privilegeDAL.AccountRepo.GetPrivilegesByAccountID(ctx, userID)
+
+ if err != nil {
+ return err
+ }
+
+ // TODO: refactor getting accountId
+
+ accountId, err := w.privilegeDAL.AccountRepo.GetAccountByID(ctx, userID)
+ if err != nil {
+ return err
+ }
+
+ currentPrivilegeMap := make(map[string]*model.ShortPrivilege)
+ for i := range currentPrivileges {
+ currentPrivilegeMap[currentPrivileges[i].PrivilegeName] = ¤tPrivileges[i]
+ }
+
+ for _, receivedPrivilege := range privilege {
+ fmt.Println("KONSUMERl", privilege, receivedPrivilege.PrivilegeID, wctools.FindPrivilegeName(receivedPrivilege.PrivilegeID))
+ if matchingCurrentPrivilege, found := currentPrivilegeMap[receivedPrivilege.PrivilegeID]; found {
+ matchingCurrentPrivilege.Amount += receivedPrivilege.Amount
+ matchingCurrentPrivilege.CreatedAt = time.Now()
+
+ err := w.privilegeDAL.AccountRepo.UpdatePrivilege(ctx, matchingCurrentPrivilege, accountId.ID)
+ if err != nil {
+ return err
+ }
+ } else {
+ newPrivilege := &model.ShortPrivilege{
+ PrivilegeID: receivedPrivilege.PrivilegeID,
+ PrivilegeName: wctools.FindPrivilegeName(receivedPrivilege.PrivilegeID),
+ Amount: receivedPrivilege.Amount,
+ CreatedAt: time.Now(),
+ }
+
+ err := w.privilegeDAL.AccountRepo.InsertPrivilege(ctx, newPrivilege, accountId.ID)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ fmt.Println("RESET STALE", w.resetStaleMessages(ctx, accountId.ID))
+
+ return nil
+}
+
+func (w *KafkaConsumerWorker) resetStaleMessages(ctx context.Context, accountID string) error {
+ keys, err := w.redis.Keys(ctx, accountID+":*").Result()
+ if err != nil {
+ return err
+ }
+ for _, key := range keys {
+ renameRes := w.redis.Rename(ctx, key, strings.TrimPrefix(key, accountID+":"))
+ if renameRes == nil {
+ return renameRes.Err()
+ }
+ }
+
+ return nil
+}
diff --git a/savewc/for_client.go b/savewc/for_client.go
new file mode 100644
index 0000000..173c950
--- /dev/null
+++ b/savewc/for_client.go
@@ -0,0 +1,72 @@
+package savewc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/themakers/hlog"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "time"
+)
+
+type DepsForClient struct {
+ WorkerSendClientCh chan model.Answer
+ Redis *redis.Client
+}
+
+type SaveForClient struct {
+ deps DepsForClient
+ errChan chan<- error
+ logger hlog.Logger
+}
+
+func NewSaveClientWorker(deps DepsForClient, errChan chan<- error, logger hlog.Logger) *SaveForClient {
+ return &SaveForClient{
+ deps: deps,
+ errChan: errChan,
+ logger: logger,
+ }
+}
+
+func (w *SaveForClient) Start(ctx context.Context) {
+
+ for {
+ select {
+ case answer, ok := <-w.deps.WorkerSendClientCh:
+ if !ok {
+ return
+ }
+ fmt.Println("SAVECLINT")
+ err := w.saveAnswer(ctx, answer)
+ if err != nil {
+ fmt.Println("Error save answer")
+ w.errChan <- err
+ }
+
+ case <-ctx.Done():
+ fmt.Println("Save for client worker terminated")
+ return
+ }
+ }
+}
+
+func (w *SaveForClient) saveAnswer(ctx context.Context, answer model.Answer) error {
+ answerJSON, err := json.Marshal(answer)
+ if err != nil {
+ fmt.Println("Error marshal answer to redis", err)
+ w.errChan <- err
+ return err
+ }
+
+ key := fmt.Sprintf("answer:%d", time.Now().UnixNano())
+
+ err = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
+ if err != nil {
+ fmt.Println("Error saving answer to redis", err)
+ w.errChan <- err
+ return err
+ }
+
+ return nil
+}
diff --git a/savewc/for_respondent.go b/savewc/for_respondent.go
new file mode 100644
index 0000000..954ddab
--- /dev/null
+++ b/savewc/for_respondent.go
@@ -0,0 +1,71 @@
+package savewc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/themakers/hlog"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "time"
+)
+
+type DepsForResp struct {
+ WorkerRespondentCh chan []model.Answer
+ Redis *redis.Client
+}
+
+type SaveForRespondent struct {
+ deps DepsForResp
+ errChan chan<- error
+ logger hlog.Logger
+}
+
+func NewSaveRespWorker(deps DepsForResp, errChan chan<- error, logger hlog.Logger) *SaveForRespondent {
+ return &SaveForRespondent{
+ deps: deps,
+ errChan: errChan,
+ logger: logger,
+ }
+}
+
+func (w *SaveForRespondent) Start(ctx context.Context) {
+ for {
+ select {
+ case answer, ok := <-w.deps.WorkerRespondentCh:
+ if !ok {
+ return
+ }
+ fmt.Println("SAVERESP")
+ err := w.saveAnswers(ctx, answer)
+ if err != nil {
+ w.logger.Module("Error save answers")
+ w.errChan <- err
+ }
+
+ case <-ctx.Done():
+ w.logger.Module("Save for respondent worker terminated")
+ return
+
+ }
+ }
+}
+
+func (w *SaveForRespondent) saveAnswers(ctx context.Context, answers []model.Answer) error {
+ for _, answer := range answers {
+ answerJSON, err := json.Marshal(answer)
+ if err != nil {
+ fmt.Println("Error marshal answer", err)
+ w.errChan <- err
+ }
+
+ key := fmt.Sprintf("toRespondent:%d", time.Now().UnixNano())
+
+ err = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
+ if err != nil {
+ fmt.Println("Error setting to redis", err)
+ w.errChan <- err
+ }
+ }
+ return nil
+}
diff --git a/wctools/tools.go b/wctools/tools.go
new file mode 100644
index 0000000..dc89a90
--- /dev/null
+++ b/wctools/tools.go
@@ -0,0 +1,142 @@
+package wctools
+
+import (
+ "encoding/json"
+ "errors"
+ "github.com/golang/protobuf/proto"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model/tariff"
+ "strings"
+ "time"
+)
+
+var DaysOfWeek = map[string]string{
+ "Monday": "понедельник",
+ "Tuesday": "вторник",
+ "Wednesday": "среда",
+ "Thursday": "четверг",
+ "Friday": "пятница",
+ "Saturday": "суббота",
+ "Sunday": "воскресенье",
+}
+
+var MonthsOfYear = map[string]string{
+ "January": "января",
+ "February": "февраля",
+ "March": "марта",
+ "April": "апреля",
+ "May": "мая",
+ "June": "июня",
+ "July": "июля",
+ "August": "августа",
+ "September": "сентября",
+ "October": "октября",
+ "November": "ноября",
+ "December": "декабря",
+}
+
+func IsValidMessage(message []byte, expectedServiceKey string) ([]model.PrivilegeMessage, string, error) {
+ var decodedMessage tariff.TariffMessage
+ err := proto.Unmarshal(message, &decodedMessage)
+ if err != nil {
+ return nil, "", err
+ }
+
+ if decodedMessage.UserID == "" || len(decodedMessage.Privileges) == 0 {
+ return nil, "", errors.New("Invalid message structure")
+ }
+
+ privileges := decodedMessage.Privileges
+ userID := decodedMessage.UserID
+
+ var validPrivileges []model.PrivilegeMessage
+ for _, privilege := range privileges {
+ if IsServiceKeyValid(privilege.ServiceKey, expectedServiceKey) {
+ validPrivileges = append(validPrivileges, model.PrivilegeMessage{
+ PrivilegeID: privilege.PrivilegeID,
+ ServiceKey: privilege.ServiceKey,
+ Type: model.PrivilegeType(privilege.Type),
+ Value: privilege.Value,
+ Amount: privilege.Amount,
+ })
+ }
+ }
+
+ if len(validPrivileges) == 0 {
+ return nil, "", errors.New("No valid privileges found")
+ }
+
+ return validPrivileges, userID, nil
+}
+
+func IsServiceKeyValid(actualServiceKey, expectedServiceKey string) bool {
+ return actualServiceKey == expectedServiceKey
+}
+
+func FindPrivilegeName(privilegeID string) string {
+ for _, p := range model.Privileges {
+ if p.PrivilegeID == privilegeID {
+ return p.Name
+ }
+ }
+ return ""
+}
+
+func ProcessAnswer(answer string) (model.ResultContent, error) {
+ content := model.ResultContent{}
+ err := json.Unmarshal([]byte(answer), &content)
+ if err != nil {
+ return model.ResultContent{}, err
+ }
+ return content, nil
+}
+
+func ProcessQuiz(quiz string) (model.QuizConfig, error) {
+ quizConfig := model.QuizConfig{}
+ err := json.Unmarshal([]byte(quiz), &quizConfig)
+ if err != nil {
+ return model.QuizConfig{}, err
+ }
+ return quizConfig, nil
+}
+
+func HasUnlimitedPrivilege(privileges []model.ShortPrivilege) bool {
+ for _, privilege := range privileges {
+ if privilege.PrivilegeID == "quizUnlimTime" {
+ return IsPrivilegeExpired(privilege)
+ }
+ }
+ return false
+}
+
+func IsPrivilegeExpired(privilege model.ShortPrivilege) bool {
+ expirationTime := privilege.CreatedAt.Add(time.Duration(privilege.Amount) * 24 * time.Hour)
+
+ currentTime := time.Now()
+ return currentTime.Before(expirationTime)
+}
+
+func ExtractEmail(key string) string {
+ parts := strings.Split(key, ":")
+ if len(parts) != 2 {
+ return ""
+ }
+ return parts[1]
+}
+
+func HasQuizCntPrivilege(privileges []model.ShortPrivilege) *model.ShortPrivilege {
+ for _, privilege := range privileges {
+ if privilege.PrivilegeID == "quizCnt" && privilege.Amount > 0 {
+ return &privilege
+ }
+ }
+ return nil
+}
+
+func ToJSON(data interface{}) (string, error) {
+ result, err := json.Marshal(data)
+ if err != nil {
+ return "", err
+ }
+ return string(result), nil
+}
diff --git a/workers/shortstat/timeout.go b/workers/shortstat/timeout.go
new file mode 100644
index 0000000..f7e5fcb
--- /dev/null
+++ b/workers/shortstat/timeout.go
@@ -0,0 +1,43 @@
+package shortstat
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers"
+ "time"
+)
+
+// ShortStat struct of worker for expiration worker
+type ShortStat struct {
+ w *workers.Worker
+ d *dal.DAL
+}
+
+// New creation of worker
+func New(d *dal.DAL, p time.Duration) *ShortStat {
+ return &ShortStat{
+ w: workers.New(p),
+ d: d,
+ }
+}
+
+// Start method for starting worker with long polling from postgres
+func (t *ShortStat) Start(ctx context.Context) {
+ t.w.Start(ctx, func(ctx context.Context) error {
+ fmt.Println("SHORTSTAT1")
+ if err := t.d.WorkerRepo.WorkerStatProcess(ctx); err != nil {
+ fmt.Println("SHORTSTAT2", err)
+ if err != sql.ErrNoRows {
+ return err
+ }
+ }
+
+ return nil
+ })
+}
+
+func (t *ShortStat) ExposeErr(ctx context.Context, err *error) {
+ t.w.ExposeErr(ctx, err)
+}
diff --git a/workers/timeout/timeout.go b/workers/timeout/timeout.go
new file mode 100644
index 0000000..a9c3d71
--- /dev/null
+++ b/workers/timeout/timeout.go
@@ -0,0 +1,40 @@
+package timeout
+
+import (
+ "context"
+ "database/sql"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
+ "penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers"
+ "time"
+)
+
+// Timeout struct of worker for expiration worker
+type Timeout struct {
+ w *workers.Worker
+ d *dal.DAL
+}
+
+// New creation of worker
+func New(d *dal.DAL, p time.Duration) *Timeout {
+ return &Timeout{
+ w: workers.New(p),
+ d: d,
+ }
+}
+
+// Start method for starting worker with long polling from postgres
+func (t *Timeout) Start(ctx context.Context) {
+ t.w.Start(ctx, func(ctx context.Context) error {
+ if err := t.d.WorkerRepo.WorkerTimeoutProcess(ctx); err != nil {
+ if err != sql.ErrNoRows {
+ return err
+ }
+ }
+
+ return nil
+ })
+}
+
+func (t *Timeout) ExposeErr(ctx context.Context, err *error) {
+ t.w.ExposeErr(ctx, err)
+}
diff --git a/workers/worker.go b/workers/worker.go
new file mode 100644
index 0000000..24c6062
--- /dev/null
+++ b/workers/worker.go
@@ -0,0 +1,55 @@
+package workers
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+type Worker struct {
+ period time.Duration
+ errChan chan error
+}
+
+// New creation of worker
+func New(p time.Duration) *Worker {
+ return &Worker{
+ period: p,
+ errChan: make(chan error),
+ }
+}
+
+// Start method for starting worker with long polling from postgres
+func (t *Worker) Start(ctx context.Context, job func(ctx context.Context) error) {
+ metronome := time.Tick(t.period)
+ for {
+ select {
+ case <-metronome:
+ func() {
+ defer func() {
+ if v := recover(); v != any(nil) {
+ t.errChan <- fmt.Errorf("%v", v)
+ }
+ }()
+ if err := job(ctx); err != nil {
+ t.errChan <- err
+ }
+ }()
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func (t *Worker) ExposeErr(ctx context.Context, err *error) {
+ go func() {
+ for {
+ select {
+ case e := <-t.errChan:
+ err = &e
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+}