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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ range .AllAnswers }} + {{ if index $.QuestionsMap .AnswerID }} + + + + {{ end }} + {{end}} + + + + +
+ + +

Квиз для вашего бизнеса

+
+

+ Поступила новая заявка с квиза “{{.QuizConfig.Theme}}”! +

+
+

+ Время заявки: {{ .AnswerTime }} +

+
+ + Посмотреть в личном кабинете + +
+

+ Контакты +

+
+ + + + + + {{ if .AnswerContent.Email }} + + + + + {{ end }} + {{ if .AnswerContent.Phone }} + + + + + {{ end }} + + {{ if .AnswerContent.Telegram }} + + + + + {{ end }} + {{ if .AnswerContent.Wechat }} + + + + + {{ end }} + {{ if .AnswerContent.Viber }} + + + + + {{ end }} + {{ if .AnswerContent.Vk }} + + + + + {{ end }} + {{ if .AnswerContent.Skype }} + + + + + {{ end }} + {{ if .AnswerContent.Whatsup }} + + + + + {{ end }} + {{ if .AnswerContent.Messenger }} + + + + + {{ end }} + {{ if .AnswerContent.Address }} + + + + + {{ end }} + {{ range $key, $value := .AnswerContent.Custom }} + + + + + + {{ end }} +
+ Имя + +

+ {{ .AnswerContent.Name}} +

+
+ Email + +

+ {{ .AnswerContent.Email }} +

+
+ Телефон + + {{ .AnswerContent.Phone }} +
+ Telegram + + {{ .AnswerContent.Telegram }} +
+ Wechat + + {{ .AnswerContent.Wechat }} +
+ Viber + + {{ .AnswerContent.Viber }} +
+ Vk + + {{ .AnswerContent.Vk }} +
+ Skype + + {{ .AnswerContent.Skype }} +
+ Whatsup + + {{ .AnswerContent.Whatsup }} +
+ Messenger + + {{ .AnswerContent.Messenger }} +
+ Адрес + + {{ .AnswerContent.Address }} +
+ {{ $key }} + + {{ $value }} +
+
+

+ Ответы +

+
+ + + + + + + + +
+

+ {{ index $.QuestionsMap .AnswerID }} +

+
+ {{ .Content }} +
+
+ + 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 + } + } + }() +}