first adding

This commit is contained in:
Pavel 2024-02-19 21:20:09 +03:00
parent e9cfbac2f3
commit 751c74a087
30 changed files with 3370 additions and 0 deletions

21
.gitignore vendored Normal file

@ -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

16
Dockerfile Normal file

@ -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"]

191
answerwc/mail/reminder.tmpl Normal file

@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* Сброс стилей */
body,
h1,
h2,
h3,
p,
div,
img,
button,
table,
th,
td {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
body {
background-color: #f2f2f7;
font-family: Arial, sans-serif;
}
@media (max-width: 400px) {
h1 {
font-size: 25px !important;
}
h4 {
font-size: 20px !important;
}
.balance {
font-size: 15px !important;
}
.image {
max-width: 223px;
height: 208px;
}
}
</style>
</head>
<body style="background-color: #f2f2f7; font-family: Arial, sans-serif">
<table style="width: 100%; padding: 16px">
<tr>
<td>
<img class="image" style="width: 103px; height: 40px" src="https://storage.yandexcloud.net/squizimages/logo-email-squiz.png" />
</td>
<td>
<p style="text-align: end; color: #9a9aaf; font-size: 14px">Квиз для вашего бизнеса</p>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<h1
style="
font-size: 30px;
font-weight: 600;
margin-bottom: 13px;
width: 100%;
margin: 0;
margin-bottom: 16px;
margin-top: 50px;
"
>
Поступила новая заявка с квиза “{{ .QuizConfig.Theme }}”!
</h1>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<p class="balance" style="color: #4d4d4d; font-size: 20px; margin-bottom: 30px">
Но у вас закончились средства на балансе :(
</p>
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center; padding: 0">
<img class="image" style="width: 100%; max-width: 440px; height: 280px; margin-bottom: 40px" src="https://storage.yandexcloud.net/squizimages/img_wallet.png" />
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<h1
style="font-size: 25px; font-weight: 600; margin-bottom: 15px; width: 100%; margin: 0; margin-bottom: 13px"
>
Аккаунт
</h1>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 0">
<table
style="
background-color: #fff;
border-radius: 8px;
text-align: left;
max-width: 480px;
width: 100%;
padding: 16px;
margin-bottom: 40px;
"
>
<tr>
<th
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Email
</th>
<td style="word-break: break-word">
<p
style="
text-align: start;
color: #7e2aea;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-bottom: 15px;
"
>
{{ .QuizConfig.Reply }}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<p class="balance" style="color: #9a9aaf; font-size: 20px; margin-bottom: 30px; text-align: center">
Пополните баланс и посмотрите заявку в личном кабинете:
</p>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%; text-align: center">
<a
style="
max-width: 312px;
color: #f2f3f7;
text-align: center;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 24px;
border-radius: 8px;
border: 1px solid #7e2aea;
background: #7e2aea;
padding: 10px 43px;
"
>
Посмотреть в личном кабинете
</a>
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center; padding: 30px 0 0 0">
<hr style="border-top: 2px solid rgba(126, 42, 234, 0.2); margin: 0 0 10px" />
<a style="color: #7e2aea; font-size: 20px; font-style: normal; font-weight: 400; line-height: normal">
quiz.pena.digital
</a>
</td>
</tr>
</table>
</body>
</html>

@ -0,0 +1,537 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* Сброс стилей */
body,
h1,
h2,
h3,
p,
div,
img,
button,
table,
th,
td {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
body {
background-color: #f2f2f7;
font-family: Arial, sans-serif;
}
@media (max-width: 600px) {
h1 {
font-size: 25px !important;
}
h4 {
font-size: 20px !important;
}
}
</style>
</head>
<body style="background-color: #f2f2f7; font-family: Arial, sans-serif">
<table style="width: 100%; padding: 16px">
<tr>
<td>
<img class="image" style="width: 103px; height: 40px" src="https://storage.yandexcloud.net/squizimages/logo-email-squiz.png" />
</td>
<td>
<p style="text-align: end; color: #9a9aaf; font-size: 14px">Квиз для вашего бизнеса</p>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<h1
style="
font-size: 30px;
font-weight: 600;
margin-bottom: 13px;
width: 100%;
margin: 0;
margin-bottom: 13px;
margin-top: 50px;
"
>
Поступила новая заявка с квиза “{{.QuizConfig.Theme}}”!
</h1>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<p style="color: #9a9aaf; font-size: 20px; margin-bottom: 50px">
Время заявки: {{ .AnswerTime }}
</p>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<a
style="
display: flex;
justify-content: center;
color: #f2f3f7;
text-align: center;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 24px;
border-radius: 8px;
border: 1px solid #7e2aea;
background: #7e2aea;
padding: 10px 43px;
max-height: 63px;
margin-bottom: 50px;
"
>
Посмотреть в личном кабинете
</a>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<h1
style="font-size: 25px; font-weight: 600; margin-bottom: 15px; width: 100%; margin: 0; margin-bottom: 13px"
>
Контакты
</h1>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 0">
<table
style="
background-color: #fff;
border-radius: 8px;
text-align: left;
max-width: 480px;
width: 100%;
padding: 16px;
margin-bottom: 30px;
"
>
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Имя
</th>
<td>
<p
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-bottom: 15px;
"
>
{{ .AnswerContent.Name}}
</p>
</td>
</tr>
{{ if .AnswerContent.Email }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Email
</th>
<td style="word-break: break-word">
<p
style="
text-align: start;
color: #7e2aea;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-bottom: 15px;
"
>
{{ .AnswerContent.Email }}
</p>
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Phone }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Телефон
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Phone }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Telegram }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Telegram
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Telegram }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Wechat }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Wechat
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Wechat }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Viber }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Viber
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Viber }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Vk }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Vk
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Vk }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Skype }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Skype
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Skype }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Whatsup }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Whatsup
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Whatsup }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Messenger }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Messenger
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Messenger }}
</td>
</tr>
{{ end }}
{{ if .AnswerContent.Address }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
Адрес
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ .AnswerContent.Address }}
</td>
</tr>
{{ end }}
{{ range $key, $value := .AnswerContent.Custom }}
<tr>
<th
style="
text-align: start;
color: #9a9aaf;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ $key }}
</th>
<td
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
"
>
{{ $value }}
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
<tr>
<td colspan="2" style="height: 100%">
<h1
style="font-size: 25px; font-weight: 600; margin-bottom: 15px; width: 100%; margin: 0; margin-bottom: 13px"
>
Ответы
</h1>
</td>
</tr>
{{ range .AllAnswers }}
{{ if index $.QuestionsMap .AnswerID }}
<tr>
<td colspan="2" style="padding: 0">
<table
style="
background-color: #fff;
border-radius: 8px;
text-align: left;
max-width: 480px;
width: 100%;
padding: 16px;
margin-bottom: 15px;
"
>
<tr>
<th colspan="2">
<p
style="
text-align: start;
color: #4d4d4d;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-bottom: 10px;
"
>
{{ index $.QuestionsMap .AnswerID }}
</p>
</th>
</tr>
<tr>
<td style="color: #9a9aaf; font-size: 20px; font-style: normal; font-weight: 400; line-height: normal">
{{ .Content }}
</td>
</tr>
</table>
</td>
</tr>
{{ end }}
{{end}}
<tr>
<td colspan="2" style="text-align: center; padding: 0">
<a style="color: #7e2aea; font-size: 20px; font-style: normal; font-weight: 400; line-height: normal">
quiz.pena.digital
</a>
</td>
</tr>
</table>
</body>
</html>

170
answerwc/respondent.go Normal file

@ -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
}
}

373
answerwc/to_client.go Normal file

@ -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
}
}

209
app/app.go Normal file

@ -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
}

10
app/logrecords.go Normal file

@ -0,0 +1,10 @@
package app
type InfoSvcStarted struct{}
type InfoSvcReady struct{}
type InfoSvcShutdown struct {
Signal string
}
type ErrorCanNotServe struct {
Err error
}

@ -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
}

@ -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",
}

@ -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
}

@ -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()
}

@ -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[:])
}

@ -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

@ -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

@ -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

@ -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

@ -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:

@ -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")

45
go.mod Normal file

@ -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
)

174
go.sum Normal file

@ -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=

10
main.go Normal file

@ -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{})
}

67
privilegewc/check.go Normal file

@ -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
}
}
}

155
privilegewc/consumer.go Normal file

@ -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] = &currentPrivileges[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
}

72
savewc/for_client.go Normal file

@ -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
}

71
savewc/for_respondent.go Normal file

@ -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
}

142
wctools/tools.go Normal file

@ -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
}

@ -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)
}

@ -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)
}

55
workers/worker.go Normal file

@ -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
}
}
}()
}