merge leadSender in staging

This commit is contained in:
Pavel 2024-06-24 13:31:08 +03:00
commit e01e3f44b5
21 changed files with 1138 additions and 850 deletions

@ -5,10 +5,9 @@ import (
"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/senders"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/wctools"
"time"
)
@ -16,19 +15,17 @@ import (
type DepsRespWorker struct {
Redis *redis.Client
Dal *dal.DAL
MailClient *mailclient.Client
MailClient *senders.MailLeadSender
}
type RespWorker struct {
deps DepsRespWorker
logger hlog.Logger
errChan chan<- error
}
func NewRespWorker(deps DepsRespWorker, logger hlog.Logger, errChan chan<- error) *RespWorker {
func NewRespWorker(deps DepsRespWorker, errChan chan<- error) *RespWorker {
return &RespWorker{
deps: deps,
logger: logger,
errChan: errChan,
}
}
@ -43,7 +40,6 @@ func (w *RespWorker) Start(ctx context.Context) {
w.processPendingAnswer(ctx)
case <-ctx.Done():
w.logger.Module("To respondent worker terminated")
return
}
@ -132,7 +128,7 @@ func (w *RespWorker) processMessageToSMTP(quizConfig model.QuizConfig, questions
theme := quizConfig.Mailing.Theme
quizConfig.Mailing.Theme = quizConfig.Mailing.Reply
data := mailclient.EmailTemplateData{
data := senders.TemplateData{
QuizConfig: quizConfig.Mailing,
AnswerContent: answerContent,
AllAnswers: allAnswers,

@ -2,31 +2,32 @@ package answerwc
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"github.com/themakers/hlog"
"github.com/go-redis/redis/v8"
"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/customer"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/clients/mailclient"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/senders"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/wctools"
"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/customer_clients"
"time"
"github.com/go-redis/redis/v8"
)
type DepsSendToClient struct {
Redis *redis.Client
Dal *dal.DAL
MailClient *mailclient.Client
CustomerService customer.CustomerServiceClient
LeadSenders []senders.LeadSender
CustomerService *customer_clients.CustomersClient
}
type SendToClient struct {
deps DepsSendToClient
logger hlog.Logger
redis *redis.Client
dal *dal.DAL
leadSenders []senders.LeadSender
customerService *customer_clients.CustomersClient
errChan chan<- error
}
@ -41,10 +42,11 @@ var toClientTemplate string
//go:embed mail/reminder.tmpl
var reminderTemplate string
func NewSendToClient(deps DepsSendToClient, logger hlog.Logger, errChan chan<- error) *SendToClient {
func NewSendToClient(deps DepsSendToClient, errChan chan<- error) *SendToClient {
return &SendToClient{
deps: deps,
logger: logger,
redis: deps.Redis,
dal: deps.Dal,
customerService: deps.CustomerService,
errChan: errChan,
}
}
@ -59,7 +61,6 @@ func (w *SendToClient) Start(ctx context.Context) {
w.processPendingAnswer(ctx)
case <-ctx.Done():
w.logger.Module("To client worker terminated")
return
}
@ -67,7 +68,7 @@ func (w *SendToClient) Start(ctx context.Context) {
}
func (w *SendToClient) processPendingAnswer(ctx context.Context) {
pendingAnswers, err := w.deps.Redis.Keys(ctx, "answer:*").Result()
pendingAnswers, err := w.redis.Keys(ctx, "answer:*").Result()
if err != nil {
fmt.Println("Error getting keys from redis")
w.errChan <- err
@ -79,7 +80,7 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
for _, key := range pendingAnswers {
func() {
fmt.Println("ANS1", key)
answerJSON, err := w.deps.Redis.GetDel(ctx, key).Result()
answerJSON, err := w.redis.GetDel(ctx, key).Result()
if err == redis.Nil {
return
} else if err != nil {
@ -91,7 +92,7 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
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()
_ = w.redis.Set(ctx, key, answerJSON, 0).Err()
}
}()
@ -110,14 +111,14 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
return
}
allAnswers, err := w.deps.Dal.WorkerAnsRepo.GetAllAnswersByQuizID(ctx, answer.Session)
allAnswers, err := w.dal.AnswerRepo.GetAllAnswersByQuizID(ctx, answer.Session)
fmt.Println("ANS4", err)
if err != nil {
w.reportError(err, "Error getting all answers by quizID")
return
}
questionsMap, sortedallAnswers, err := w.deps.Dal.QuestionRepo.GetMapQuestions(ctx, allAnswers)
questionsMap, sortedallAnswers, err := w.dal.QuestionRepo.GetMapQuestions(ctx, allAnswers)
fmt.Println("ANS5", err)
if err != nil {
w.reportError(err, "Error getting questionsMap")
@ -128,14 +129,14 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
return
}
quizConfig, accountId, err := w.deps.Dal.QuizRepo.GetQuizConfig(ctx, answer.QuizId)
quizConfig, accountId, err := w.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)
quiz, err := w.dal.QuizRepo.GetQuizById(ctx, accountId, answer.QuizId)
fmt.Println("ANS60", err, accountId, answer.QuizId)
if err != nil {
w.reportError(err, "Error getting quiz")
@ -148,21 +149,30 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
quizConfig.Mailing.Theme = quiz.Name
}
account, privileges, err := w.deps.Dal.AccountRepo.GetAccAndPrivilegeByEmail(ctx, accountId)
account, privileges, err := w.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, sortedallAnswers, answerContent, answer.CreatedAt)
result, err := w.processAnswerWithPrivileges(ctx, ProcessAnsWithPriv{
quiz: quiz,
quizConfig: quizConfig,
questionsMap: questionsMap,
privileges: privileges,
account: account,
allAnswers: sortedallAnswers,
answerContent: answerContent,
answerTime: 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()
err = w.redis.Set(ctx, fmt.Sprintf("%s:%s", account.ID, key), answerJSON, 0).Err()
if err != nil {
w.reportError(err, "Error setting redis value")
return
@ -172,45 +182,70 @@ func (w *SendToClient) processPendingAnswer(ctx context.Context) {
}
}
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) {
type ProcessAnsWithPriv struct {
quiz *model.Quiz
quizConfig model.QuizConfig
questionsMap map[uint64]string
privileges []model.ShortPrivilege
account model.Account
allAnswers []model.ResultAnswer
answerContent model.ResultContent
answerTime time.Time
}
err := w.notificationCustomer(account, privileges)
func (w *SendToClient) processAnswerWithPrivileges(ctx context.Context, data ProcessAnsWithPriv) (bool, error) {
err := w.notificationCustomer(ctx, data.account, data.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 wctools.HasUnlimitedPrivilege(data.privileges) {
err := w.ProcessMessageToClient(ctx, DepsProcessMsgToClient{
quizConfig: data.quizConfig,
questionsMap: data.questionsMap,
account: data.account,
allAnswers: data.allAnswers,
answerContent: data.answerContent,
answerTime: data.answerTime,
quiz: data.quiz,
})
if err != nil {
return false, err
}
return true, nil
}
privilege := wctools.HasQuizCntPrivilege(privileges)
privilege := wctools.HasQuizCntPrivilege(data.privileges)
if privilege != nil {
err := w.ProcessMessageToClient(quizConfig, questionsMap, account, allAnswers, answerContent, answerTime)
err := w.ProcessMessageToClient(ctx, DepsProcessMsgToClient{
quizConfig: data.quizConfig,
questionsMap: data.questionsMap,
account: data.account,
allAnswers: data.allAnswers,
answerContent: data.answerContent,
answerTime: data.answerTime,
quiz: data.quiz,
})
fmt.Println("PMC", err)
if err != nil {
return true, err
}
privilege.Amount--
err = w.deps.Dal.AccountRepo.UpdatePrivilegeAmount(ctx, privilege.ID, privilege.Amount)
err = w.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,
w.checkAndSendTaskReminders(sendTaskRemindersDeps{
account: data.account,
theme: data.quiz.Name,
config: model.QuizConfig{
Mailing: model.ResultInfo{
When: "email",
Theme: fmt.Sprintf("не удалось отправить заявку по опросу\"%s\"", quizName),
Theme: fmt.Sprintf("не удалось отправить заявку по опросу\"%s\"", data.quiz.Name),
Reply: "noreply@pena.digital",
ReplName: "Reminder",
},
@ -224,7 +259,7 @@ func (w *SendToClient) recordPendingTasks(ctx context.Context, Email string, qui
key := fmt.Sprintf("pending_tasks:%s", Email)
var pendingTasks PendingTasks
val, err := w.deps.Redis.HGet(ctx, key, "data").Result()
val, err := w.redis.HGet(ctx, key, "data").Result()
if err == nil {
err := json.Unmarshal([]byte(val), &pendingTasks)
if err != nil {
@ -243,7 +278,7 @@ func (w *SendToClient) recordPendingTasks(ctx context.Context, Email string, qui
return err
}
err = w.deps.Redis.HSet(ctx, key, "data", string(pendingTasksJSON)).Err()
err = w.redis.HSet(ctx, key, "data", string(pendingTasksJSON)).Err()
if err != nil {
return err
}
@ -252,31 +287,30 @@ func (w *SendToClient) recordPendingTasks(ctx context.Context, Email string, qui
}
type sendTaskRemindersDeps struct {
email, theme string
account model.Account
theme string
config model.QuizConfig
}
func (w *SendToClient) checkAndSendTaskReminders(ctx context.Context, deps sendTaskRemindersDeps) {
err := w.processReminderToClient(deps.email, deps.config)
func (w *SendToClient) checkAndSendTaskReminders(data sendTaskRemindersDeps) {
err := w.processReminderToClient(data.account, data.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 {
func (w *SendToClient) notificationCustomer(ctx context.Context, 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{
historyData := customer_clients.InsertHistoryDeps{
UserID: account.UserID,
Comment: fmt.Sprintf("Привилегия %s просрочена", privilege.PrivilegeID),
Comment: fmt.Sprintf("%s privilege has expired, it was created at %d", privilege.PrivilegeID, privilege.CreatedAt.Unix()),
Key: "privilege_expired",
RawDetails: rawDetail,
}
_, err = w.deps.CustomerService.InsertHistory(context.Background(), historyData)
err := w.customerService.InsertHistory(ctx, historyData)
if err != nil {
return err
}
@ -284,18 +318,13 @@ func (w *SendToClient) notificationCustomer(account model.Account, privileges []
}
if privilege.PrivilegeID == "quizCnt" && privilege.Amount == 0 {
rawDetail, err := wctools.ToJSON(privilege)
if err != nil {
return err
}
historyData := &customer.History{
historyData := customer_clients.InsertHistoryDeps{
UserID: account.UserID,
Comment: fmt.Sprintf("У привилегии %s истек amount", privilege.PrivilegeID),
Comment: fmt.Sprintf("%s privilege has expired, it was created at %d", privilege.PrivilegeID, privilege.CreatedAt.Unix()),
Key: "privilege_expired",
RawDetails: rawDetail,
}
_, err = w.deps.CustomerService.InsertHistory(context.Background(), historyData)
err := w.customerService.InsertHistory(ctx, historyData)
if err != nil {
return err
}
@ -305,50 +334,83 @@ func (w *SendToClient) notificationCustomer(account model.Account, privileges []
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
type DepsProcessMsgToClient struct {
quizConfig model.QuizConfig
questionsMap map[uint64]string
account model.Account
allAnswers []model.ResultAnswer
answerContent model.ResultContent
answerTime time.Time
quiz *model.Quiz
}
data := mailclient.EmailTemplateData{
QuizConfig: quizConfig.Mailing,
AnswerContent: answerContent,
AllAnswers: allAnswers,
QuestionsMap: questionsMap,
// сделал экспортируемым для теста
func (w *SendToClient) ProcessMessageToClient(ctx context.Context, constructData DepsProcessMsgToClient) error {
leadTargetForAll, err := w.dal.AccountRepo.GetLeadTarget(ctx, constructData.quiz.AccountId, 0)
if err != nil {
return err
}
leadTargetForQuiz, err := w.dal.AccountRepo.GetLeadTarget(ctx, constructData.quiz.AccountId, int32(constructData.quiz.Id))
if err != nil && err != sql.ErrNoRows {
return err
}
if len(leadTargetForQuiz) > 0 {
leadTargetForAll = append(leadTargetForAll, leadTargetForQuiz...)
}
dayOfWeek := wctools.DaysOfWeek[answerTime.Format("Monday")]
monthOfYear := wctools.MonthsOfYear[answerTime.Format("January")]
theme := constructData.quizConfig.Mailing.Theme
constructData.quizConfig.Mailing.Theme = constructData.quizConfig.Mailing.Reply
data := senders.TemplateData{
QuizConfig: constructData.quizConfig.Mailing,
AnswerContent: constructData.answerContent,
AllAnswers: constructData.allAnswers,
QuestionsMap: constructData.questionsMap,
}
dayOfWeek := wctools.DaysOfWeek[constructData.answerTime.Format("Monday")]
monthOfYear := wctools.MonthsOfYear[constructData.answerTime.Format("January")]
formattedTime := fmt.Sprintf("%s, %d %s %d г., %02d:%02d (UTC%s)",
dayOfWeek,
answerTime.Day(),
constructData.answerTime.Day(),
monthOfYear,
answerTime.Year(),
answerTime.Hour(),
answerTime.Minute(),
answerTime.Format("-07:00"),
constructData.answerTime.Year(),
constructData.answerTime.Hour(),
constructData.answerTime.Minute(),
constructData.answerTime.Format("-07:00"),
)
data.AnswerTime = formattedTime
fmt.Println("SUBJECT", theme, account.Email)
mapLeadTarget := make(map[string][]senders.LeadData) // ключ имя сендера, модель отправки
for _, leadTarget := range leadTargetForAll {
mapLeadTarget[string(leadTarget.Type)] = append(mapLeadTarget[string(leadTarget.Type)], senders.LeadData{
To: leadTarget.Target,
Subject: theme,
TemplateData: data,
})
}
err := w.deps.MailClient.SendMailWithAttachment(account.Email, theme, toClientTemplate, data, nil)
for _, sender := range w.leadSenders {
for _, sendData := range mapLeadTarget[sender.Name()] {
err := sender.SendLead(sendData)
if err != nil {
return err
w.reportError(err, fmt.Sprintf("Error sending lead through %s", sender.Name()))
}
}
}
return nil
}
func (w *SendToClient) processReminderToClient(email string, quizConfig model.QuizConfig) error {
data := mailclient.EmailTemplateData{
// todo email
func (w *SendToClient) processReminderToClient(account model.Account, quizConfig model.QuizConfig) error {
data := senders.TemplateData{
QuizConfig: model.ResultInfo{
When: quizConfig.Mailing.When,
Theme: quizConfig.Mailing.Theme,
Reply: email,
Reply: "email",
ReplName: quizConfig.Mailing.ReplName,
},
AnswerContent: model.ResultContent{},
@ -356,11 +418,20 @@ func (w *SendToClient) processReminderToClient(email string, quizConfig model.Qu
QuestionsMap: nil,
}
fmt.Println("PRTC", data, email, quizConfig)
//fmt.Println("PRTC", data, email, quizConfig)
err := w.deps.MailClient.SendMailWithAttachment(email, quizConfig.Mailing.Theme, reminderTemplate, data, nil)
leadData := senders.LeadData{
To: "email",
Subject: quizConfig.Mailing.Theme,
Template: reminderTemplate,
TemplateData: data,
}
for _, sender := range w.leadSenders {
err := sender.SendLead(leadData)
if err != nil {
return err
w.reportError(err, fmt.Sprintf("Error sending lead through %s", sender.Name()))
}
}
return nil

@ -10,15 +10,15 @@ import (
"github.com/skeris/appInit"
"github.com/themakers/hlog"
"go.uber.org/zap"
"google.golang.org/grpc"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/clients"
"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/senders"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers/shortstat"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/workers/timeout"
"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/customer_clients"
"time"
)
@ -49,19 +49,16 @@ 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"`
KafkaBroker string `env:"KAFKA_BROKER" default:"localhost:6379"`
KafkaTopic string `env:"KAFKA_TOPIC" default:"test-topic"`
LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
IsProd bool `env:"IS_PROD" default:"false"`
MinioEP string `env:"MINIO_EP" default:"localhost:3002"`
MinioAK string `env:"MINIO_AK" default:"minio"`
MinioSK string `env:"MINIO_SK" default:"miniostorage"`
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
RedisHost string `env:"REDIS_HOST" default:"localhost:6379"`
RedisPassword string `env:"REDIS_PASSWORD"`
RedisPassword string `env:"REDIS_PASSWORD" default:"admin"`
RedisDB uint64 `env:"REDIS_DB" default:"2"`
SmtpHost string `env:"SMTP_HOST" default:"connect.mailclient.bz"`
SmtpPort string `env:"SMTP_PORT" default:"587"`
@ -70,7 +67,8 @@ type Options struct {
SmtpPassword string `env:"SMTP_PASSWORD" default:"vWwbCSg4bf0p"`
SmtpApiKey string `env:"SMTP_API_KEY" default:"P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev"`
SmtpApiUrl string `env:"SMTP_API_URL" default:"https://api.smtp.bz/v1/smtp/send"`
CustomerServiceAddress string `env:"CUSTOMER_SERVICE_ADDRESS"`
CustomerServiceAddress string `env:"CUSTOMER_SERVICE_ADDRESS" default:"localhost:9001"`
TgToken string `env:"TG_TOKEN"`
}
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
@ -126,21 +124,26 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
DB: int(options.RedisDB),
})
smtpData := mailclient.ClientDeps{
Host: options.SmtpHost,
Port: options.SmtpPort,
Sender: options.SmtpSender,
mailClent := clients.NewSmtpClient(clients.Deps{
SmtpHost: options.SmtpHost,
SmtpPort: options.SmtpPort,
SmtpSender: options.SmtpSender,
ApiKey: options.SmtpApiKey,
SmtpApiUrl: options.SmtpApiUrl,
}
})
mailClient := mailclient.NewClient(smtpData)
customerServiceConn, err := grpc.Dial(options.CustomerServiceAddress, grpc.WithInsecure())
tgSender, err := senders.NewTgSender(options.TgToken)
if err != nil {
fmt.Println(err)
return nil, err
}
customerServiceClient := customer.NewCustomerServiceClient(customerServiceConn)
mailSender := senders.NewMailLeadSender(mailClent)
leadSenders := []senders.LeadSender{mailSender, tgSender}
customerClient := customer_clients.NewCustomersClient(customer_clients.CustomersClientDeps{
Logger: zapLogger,
CustomerServiceHost: options.CustomerServiceAddress,
})
minioClient, err := minio.New(options.MinioEP, &minio.Options{
Creds: credentials.NewStaticV4(options.MinioAK, options.MinioSK, ""),
@ -161,7 +164,6 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
KafkaTopic: options.KafkaTopic,
ServiceKey: options.ServiceName,
TickerInterval: time.Second * 10,
Logger: logger,
ErrChan: errChan,
}, redisClient, pgdal)
if err != nil {
@ -169,31 +171,29 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
return nil, err
}
checkWorker := privilegewc.NewCheckWorker(privilegewc.CheckWorkerConfig{
DefaultData: model.DefaultData{
PrivilegeID: options.PrivilegeID,
Amount: options.Amount,
UnlimID: options.UnlimID,
},
checkWorker := privilegewc.NewCheckWorker(privilegewc.Deps{
PrivilegeIDsDays: []string{"quizUnlimTime", "squizHideBadge"},
PrivilegeIDsCount: []string{"quizCnt", "quizManual"},
TickerInterval: time.Minute,
Logger: logger,
ErrChan: errChan,
}, pgdal)
PrivilegeDAL: pgdal,
CustomerClient: customerClient,
}, errChan)
go kafkaWorker.Start(ctx)
go checkWorker.Start(ctx)
toClientWorker := answerwc.NewSendToClient(answerwc.DepsSendToClient{
Redis: redisClient,
Dal: pgdal,
MailClient: mailClient,
CustomerService: customerServiceClient,
}, logger, errChan)
LeadSenders: leadSenders,
CustomerService: customerClient,
}, errChan)
toRespWorker := answerwc.NewRespWorker(answerwc.DepsRespWorker{
Redis: redisClient,
Dal: pgdal,
MailClient: mailClient,
}, logger, errChan)
MailClient: mailSender,
}, errChan)
go toClientWorker.Start(ctx)
go toRespWorker.Start(ctx)

@ -1,182 +0,0 @@
// 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
}

@ -1,108 +0,0 @@
// 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",
}

@ -1,131 +0,0 @@
package mailclient
import (
"bytes"
_ "embed"
"fmt"
"github.com/gofiber/fiber/v2"
"html/template"
"mime/multipart"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
)
type ClientDeps struct {
Host string
Port string
Sender string
ApiKey string
SmtpApiUrl string
FiberClient *fiber.Client
}
type Client struct {
deps ClientDeps
}
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 {
sanitizedData := sanitizeHTMLData(data)
text, err := generateTextFromTemplate(sanitizedData, emailTemplate)
if err != nil {
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 {
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 {
return err
}
}
// пока не используется так как Attachments из воркеров = nil, нужен ли будет потом
for _, attachment := range msg.Attachments {
part, err := writer.CreateFormFile("attachments", attachment.Name)
if err != nil {
return err
}
_, err = part.Write(attachment.Data)
if err != nil {
return err
}
}
if err := writer.Close(); err != nil {
return err
}
agent := c.deps.FiberClient.Post(c.deps.SmtpApiUrl).Body(form.Bytes()).ContentType(writer.FormDataContentType())
if c.deps.ApiKey != "" {
agent.Set("Authorization", c.deps.ApiKey)
}
statusCode, body, errs := agent.Bytes()
if errs != nil {
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").Funcs(tmplFuncs).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
}

@ -1,104 +0,0 @@
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()
}

@ -78,7 +78,7 @@ services:
init: true
build:
context: ../..
dockerfile: TestsDockerfile
dockerfile: testDockerfile
depends_on:
test-postgres:
condition: service_healthy
@ -86,8 +86,8 @@ services:
# condition: service_healthy
# volumes:
# - ./../..:/app:ro
# command: [ "go", "test", "./tests", "-run", "TestFoo" ]
command: [ "go", "test", "-parallel", "1", "./tests" ]
# command: [ "go", "test", "./test", "-run", "TestFoo" ]
command: [ "go", "test", "-parallel", "1", "./test" ]
networks:
- penatest

25
go.mod

@ -1,23 +1,21 @@
module penahub.gitlab.yandexcloud.net/backend/quiz/worker.git
go 1.21.4
go 1.22.0
toolchain go1.22.2
require (
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang/protobuf v1.5.4
github.com/minio/minio-go/v7 v7.0.69
github.com/pioz/faker v1.7.3
github.com/skeris/appInit v1.0.2
github.com/stretchr/testify v1.8.4
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
github.com/twmb/franz-go v1.17.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.25.0
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.1
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240617173955-a34278088ddd
golang.org/x/net v0.23.0
gopkg.in/tucnak/telebot.v2 v2.5.0
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240612083524-11882ffe22cf
penahub.gitlab.yandexcloud.net/pena-services/customer v1.0.1-0.20240608222239-5c78187bf014
)
require (
@ -25,10 +23,10 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
@ -38,10 +36,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.69 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
@ -50,11 +50,14 @@ require (
github.com/valyala/fasthttp v1.54.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240527160654-bd1c2126bc6c // indirect
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c // indirect
)

31
go.sum

@ -32,8 +32,9 @@ github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -69,12 +70,16 @@ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
github.com/pioz/faker v1.7.3 h1:Tez8Emuq0UN+/d6mo3a9m/9ZZ/zdfJk0c5RtRatrceM=
github.com/pioz/faker v1.7.3/go.mod h1:xSpay5w/oz1a6+ww0M3vfpe40pSIykeUPeWEc3TvVlc=
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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/skeris/appInit v1.0.2 h1:Hr4KbXYd6kolTVq4cXGqDpgnpmaauiOiKizA1+Ep4KQ=
@ -82,11 +87,10 @@ github.com/skeris/appInit v1.0.2/go.mod h1:4ElEeXWVGzU3dlYq/eMWJ/U5hd+LKisc1z3+y
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.17.0 h1:hawgCx5ejDHkLe6IwAtFWwxi3OU4OztSTl7ZV5rwkYk=
github.com/twmb/franz-go v1.17.0/go.mod h1:NreRdJ2F7dziDY/m6VyspWd6sNxHKXdMZI42UfQ3GXM=
github.com/twmb/franz-go/pkg/kmsg v1.8.0 h1:lAQB9Z3aMrIP9qF9288XcFf/ccaSxEitNA1CDTEIeTA=
@ -116,8 +120,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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=
@ -141,13 +145,16 @@ google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLp
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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/tucnak/telebot.v2 v2.5.0 h1:i+NynLo443Vp+Zn3Gv9JBjh3Z/PaiKAQwcnhNI7y6Po=
gopkg.in/tucnak/telebot.v2 v2.5.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8=
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=
@ -155,9 +162,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
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-20240527160654-bd1c2126bc6c h1:jxnyIeC2CNDNmfdFx2qnLS4Qd0v5ocYrY9X+OL9qsvc=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240527160654-bd1c2126bc6c/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240617151847-e1322a679dd1 h1:KCQf7PmSq63YemEF5s81f7lXsewWFQjIr0IeIWxzBVo=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240617151847-e1322a679dd1/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240617173955-a34278088ddd h1:/FW9GjEbxXWD5/e3oUGn4iKihhc77FvfDPnVqL1SQvM=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240617173955-a34278088ddd/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c h1:CWb4UcuNXhd1KTNOmy2U0TJO4+Qxgxrj5cwkyFqbgrk=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c/go.mod h1:+bPxq2wfW5S1gd+83vZYmHm33AE7nEBfznWS8AM1TKE=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240612083524-11882ffe22cf h1:cTmv0YZE1B+ofsWfHYEiNxzToWKMy12rVW3cPOrtp30=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240612083524-11882ffe22cf/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/pena-services/customer v1.0.1-0.20240608222239-5c78187bf014 h1:ziG55nv824SGFZ02AfagKQC5D4ODirGXnpVPQTL6YFA=
penahub.gitlab.yandexcloud.net/pena-services/customer v1.0.1-0.20240608222239-5c78187bf014/go.mod h1:hIMkN5Xe01vAVaX22QWsGD87Oi93IfX1hJGqxy0oJbE=

@ -3,40 +3,49 @@ 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"
"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/customer_clients"
"time"
)
type CheckWorkerConfig struct {
type Deps struct {
PrivilegeDAL *dal.DAL
TickerInterval time.Duration
DefaultData model.DefaultData
Logger hlog.Logger
ErrChan chan<- error
PrivilegeIDsDays []string
PrivilegeIDsCount []string
CustomerClient *customer_clients.CustomersClient
}
type CheckWorker struct {
config CheckWorkerConfig
privilegeDAL *dal.DAL
tickerInterval time.Duration
errChan chan<- error
privilegeIDsDays []string
privilegeIDsCount []string
customerClient *customer_clients.CustomersClient
}
func NewCheckWorker(config CheckWorkerConfig, privilegeDAL *dal.DAL) *CheckWorker {
func NewCheckWorker(deps Deps, errChan chan<- error) *CheckWorker {
return &CheckWorker{
config: config,
privilegeDAL: privilegeDAL,
privilegeDAL: deps.PrivilegeDAL,
tickerInterval: deps.TickerInterval,
errChan: errChan,
privilegeIDsCount: deps.PrivilegeIDsCount,
privilegeIDsDays: deps.PrivilegeIDsDays,
customerClient: deps.CustomerClient,
}
}
func (w *CheckWorker) Start(ctx context.Context) {
ticker := time.NewTicker(w.config.TickerInterval)
ticker := time.NewTicker(w.tickerInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("CHECK")
w.performScheduledTasks(ctx)
w.deleteExpired(ctx)
case <-ctx.Done():
fmt.Println("Check worker terminated")
return
@ -44,24 +53,44 @@ func (w *CheckWorker) Start(ctx context.Context) {
}
}
// 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)
var toHistory []customer_clients.InsertHistoryDeps
var expiredData []model.ExpiredPrivileges
for _, id := range w.privilegeIDsDays {
expired, err := w.privilegeDAL.AccountRepo.GetExpired(ctx, id)
if err != nil {
w.config.Logger.Module("Error getting expired quizUnlimTime records")
w.config.ErrChan <- err
w.errChan <- err
}
expiredData = append(expiredData, expired...)
}
for _, id := range w.privilegeIDsCount {
expired, err := w.privilegeDAL.AccountRepo.GetExpiredCount(ctx, id)
if err != nil {
w.errChan <- err
}
expiredData = append(expiredData, expired...)
}
for _, data := range expiredData {
err := w.privilegeDAL.AccountRepo.DeletePrivilegeByID(ctx, data.Privilege.ID)
if err != nil {
w.config.Logger.Module("Error deleting expired quizUnlimTime record")
w.config.ErrChan <- err
w.errChan <- err
continue
}
toHistory = append(toHistory, customer_clients.InsertHistoryDeps{
UserID: data.UserID,
Comment: fmt.Sprintf("%s privilege has expired, it was created at %d", data.Privilege.PrivilegeID, data.Privilege.CreatedAt.Unix()),
Key: "privilege_expired",
})
}
for _, to := range toHistory {
err := w.customerClient.InsertHistory(ctx, to)
if err != nil {
w.errChan <- err
}
}
}

@ -4,7 +4,6 @@ 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"
@ -19,7 +18,6 @@ type Config struct {
KafkaTopic string
ServiceKey string
TickerInterval time.Duration
Logger hlog.Logger
ErrChan chan<- error
}

@ -1,71 +1,63 @@
package mailclient
package senders
import (
"bytes"
"crypto/rand"
_ "embed"
"encoding/json"
"fmt"
"golang.org/x/net/html"
"html/template"
"io"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
)
type LineWriter struct {
w io.Writer
length int
//go:embed template/client_mail.tmpl
var toClientMailTemplate string
//go:embed template/client_tg.tmpl
var toClientTgTemplate string
//go:embed template/client_whatsapp.tmpl
var toClientWhatsAppTemplate string
type LeadSender interface {
SendLead(leadData LeadData) error
Name() string
}
func NewLineWriter(w io.Writer, length int) *LineWriter {
return &LineWriter{
w: w,
length: length,
}
type LeadData struct {
To interface{}
Subject string
Template string
TemplateData TemplateData
}
func (r *LineWriter) Write(p []byte) (n int, err error) {
for i := 0; i < len(p); i += r.length {
end := i + r.length
type TemplateData struct {
QuizConfig model.ResultInfo
AnswerContent model.ResultContent
AllAnswers []model.ResultAnswer
QuestionsMap map[uint64]string
AnswerTime string
}
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)
func generateTextFromTemplate(data TemplateData, tpl string) (string, error) {
t, err := template.New("email").Funcs(tmplFuncs).Parse(tpl)
if err != nil {
return n, err
}
n += addN
return "", fmt.Errorf("error parsing template: %w", err)
}
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)
var text bytes.Buffer
if err := t.Execute(&text, TemplateData{
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 fmt.Sprintf("%x", buf[:])
return text.String(), nil
}
var tmplFuncs = template.FuncMap{
@ -92,8 +84,8 @@ func SplitContent(content string) template.HTML {
return template.HTML(content)
}
func sanitizeHTMLData(data EmailTemplateData) EmailTemplateData {
sanitized := EmailTemplateData{
func sanitizeHTMLData(data TemplateData) TemplateData {
sanitized := TemplateData{
QuizConfig: stripHTMLResultInfo(data.QuizConfig),
AnswerContent: stripHTMLResultContent(data.AnswerContent),
AllAnswers: stripHTMLResultAnswers(data.AllAnswers),

41
senders/mail_sender.go Normal file

@ -0,0 +1,41 @@
package senders
import "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/clients"
type MailLeadSender struct {
client *clients.SmtpClient
}
func NewMailLeadSender(client *clients.SmtpClient) *MailLeadSender {
return &MailLeadSender{client: client}
}
func (m *MailLeadSender) SendLead(data LeadData) error {
err := m.SendMailWithAttachment(data.To.(string), data.Subject, toClientMailTemplate, data.TemplateData, nil)
if err != nil {
return err
}
return nil
}
func (m *MailLeadSender) SendMailWithAttachment(recipient, subject string, emailTemplate string, data TemplateData, attachments []clients.Attachment) error {
sanitizedData := sanitizeHTMLData(data)
text, err := generateTextFromTemplate(sanitizedData, emailTemplate)
if err != nil {
return err
}
msg := clients.Message{
To: recipient,
Subject: subject,
HtmlBody: text,
Attachments: attachments,
}
return m.client.MailSender(msg)
}
func (m *MailLeadSender) Name() string {
return "mail"
}

@ -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">
{{ renderImage .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>

@ -0,0 +1,26 @@
*Квиз для вашего бизнеса*
*Поступила новая заявка с квиза "{{.QuizConfig.Theme}}"!*
Время заявки: {{ .AnswerTime }}
Имя: {{ .AnswerContent.Name }}
{{ if .AnswerContent.Email }}Email: {{ .AnswerContent.Email }}{{ end }}
{{ if .AnswerContent.Phone }}Телефон: {{ .AnswerContent.Phone }}{{ end }}
{{ if .AnswerContent.Telegram }}Telegram: {{ .AnswerContent.Telegram }}{{ end }}
{{ if .AnswerContent.Wechat }}Wechat: {{ .AnswerContent.Wechat }}{{ end }}
{{ if .AnswerContent.Viber }}Viber: {{ .AnswerContent.Viber }}{{ end }}
{{ if .AnswerContent.Vk }}Vk: {{ .AnswerContent.Vk }}{{ end }}
{{ if .AnswerContent.Skype }}Skype: {{ .AnswerContent.Skype }}{{ end }}
{{ if .AnswerContent.Whatsup }}Whatsup: {{ .AnswerContent.Whatsup }}{{ end }}
{{ if .AnswerContent.Messenger }}Messenger: {{ .AnswerContent.Messenger }}{{ end }}
{{ if .AnswerContent.Address }}Адрес: {{ .AnswerContent.Address }}{{ end }}
{{ range $key, $value := .AnswerContent.Custom }}{{ $key }}: {{ $value }}{{ end }}
*Ответы:*
{{ range .AllAnswers }}
{{ if index $.QuestionsMap .AnswerID }}
*{{ index $.QuestionsMap .AnswerID }}*
{{ .Content }}
{{ end }}
{{ end }}

45
senders/tg_sender.go Normal file

@ -0,0 +1,45 @@
package senders
import (
"fmt"
"gopkg.in/tucnak/telebot.v2"
"time"
)
type TgSender struct {
bot *telebot.Bot
}
func NewTgSender(tgToken string) (*TgSender, error) {
bot, err := telebot.NewBot(telebot.Settings{
Token: tgToken,
Poller: &telebot.LongPoller{Timeout: 10 * time.Second},
})
if err != nil {
return nil, fmt.Errorf("error creating Telegram bot: %w", err)
}
return &TgSender{bot: bot}, nil
}
func (tg *TgSender) SendLead(data LeadData) error {
text, err := generateTextFromTemplate(data.TemplateData, toClientTgTemplate)
if err != nil {
return err
}
chat := data.To.(int64)
_, err = tg.bot.Send(telebot.ChatID(chat), text, &telebot.SendOptions{
ParseMode: telebot.ModeHTML,
})
if err != nil {
return fmt.Errorf("error sending Telegram message: %w", err)
}
return nil
}
func (tg *TgSender) Name() string {
return "telegram"
}

@ -0,0 +1,19 @@
package senders
import "fmt"
type WhatsAppSender struct {
}
func NewWhatsAppSender() *WhatsAppSender {
return &WhatsAppSender{}
}
func (wa *WhatsAppSender) SendLead(data LeadData) error {
fmt.Println("TODO")
return nil
}
func (wa *WhatsAppSender) Name() string {
return "whatsapp"
}

@ -3,11 +3,9 @@ package tests
import (
_ "embed"
"github.com/gofiber/fiber/v2"
"github.com/pioz/faker"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/clients"
"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/mailclient"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/senders"
"testing"
"time"
)
@ -19,21 +17,21 @@ var toClientTemplate string
var reminderTemplate string
func TestProcessMessageToSMTP(t *testing.T) {
clientDeps := mailclient.ClientDeps{
Host: "connect.mailclient.bz",
Port: "587",
Sender: "skeris@mailing.pena.digital",
clientDeps := clients.Deps{
SmtpHost: "connect.mailclient.bz",
SmtpPort: "587",
SmtpSender: "skeris@mailing.pena.digital",
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
SmtpApiUrl: "https://api.smtp.bz/v1/smtp/send",
FiberClient: &fiber.Client{},
}
client := mailclient.NewClient(clientDeps)
client := clients.NewSmtpClient(clientDeps)
recipient := "pashamullin2001@gmail.com"
subject := "Test"
data := mailclient.EmailTemplateData{
data := senders.TemplateData{
QuizConfig: model.ResultInfo{
Theme: "<h1>Taemplste Quiz</h1>",
},
@ -65,7 +63,8 @@ func TestProcessMessageToSMTP(t *testing.T) {
AnswerTime: time.Now().Format("Monday, 2 January 2006 г., 15:04 UTC-07:00"),
}
err := client.SendMailWithAttachment(recipient, subject, toClientTemplate, data, nil)
mailSender := senders.NewMailLeadSender(client)
err := mailSender.SendMailWithAttachment(recipient, subject, toClientTemplate, data, nil)
if err != nil {
t.Errorf("Error sending email: %v", err)
}
@ -73,15 +72,15 @@ func TestProcessMessageToSMTP(t *testing.T) {
}
func TestProcessReminderToClient(t *testing.T) {
clientDeps := mailclient.ClientDeps{
Host: "connect.mailclient.bz",
Port: "587",
Sender: "skeris@mailing.pena.digital",
clientDeps := clients.Deps{
SmtpHost: "connect.mailclient.bz",
SmtpPort: "587",
SmtpSender: "skeris@mailing.pena.digital",
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
FiberClient: &fiber.Client{},
}
client := mailclient.NewClient(clientDeps)
client := clients.NewSmtpClient(clientDeps)
recipient := "mullinp@internet.ru"
subject := "Test Reminder"
@ -92,7 +91,9 @@ func TestProcessReminderToClient(t *testing.T) {
Theme: "Reminder Theme",
}
err := client.SendMailWithAttachment(recipient, subject, reminderTemplate, mailclient.EmailTemplateData{
mailSender := senders.NewMailLeadSender(client)
err := mailSender.SendMailWithAttachment(recipient, subject, reminderTemplate, senders.TemplateData{
QuizConfig: quizConfig,
AnswerContent: model.ResultContent{},
AllAnswers: []model.ResultAnswer{},
@ -104,72 +105,73 @@ func TestProcessReminderToClient(t *testing.T) {
}
}
func TestProcessMessageToClient(t *testing.T) {
smtpData := mailclient.ClientDeps{
Host: "connect.mailclient.bz",
Port: "587",
Sender: "skeris@mailing.pena.digital",
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
FiberClient: &fiber.Client{},
}
mailClient := mailclient.NewClient(smtpData)
deps := answerwc.DepsSendToClient{
Redis: nil,
Dal: nil,
MailClient: mailClient,
}
errChan := make(chan<- error)
w := answerwc.NewSendToClient(deps, nil, errChan)
quizConfig := model.QuizConfig{
Mailing: model.ResultInfo{
Theme: faker.String(),
},
}
questionsMap := map[uint64]string{
1: faker.String(),
2: faker.String(),
}
account := model.Account{
Email: "pashamullin2001@gmail.com",
}
allAnswers := []model.ResultAnswer{
{
Content: `{"Image":"https://letsenhance.io/static/8f5e523ee6b2479e26ecc91b9c25261e/1015f/MainAfter.jpg","Description":"Gekon"}`,
AnswerID: 1,
QuestionID: 1,
},
{
AnswerID: 2,
QuestionID: 2,
},
}
answerContent := model.ResultContent{
Name: "Pasha",
Phone: "+723456789",
Email: "test@example.com",
//Adress: "chtoto tam",
Telegram: "@test",
Wechat: "test_wechat",
Viber: "+723456789",
Vk: "test_vk",
Skype: "test_skype",
Whatsup: "test_whatsup",
Messenger: "test_messenger",
}
answerTime := time.Now()
err := w.ProcessMessageToClient(quizConfig, questionsMap, account, allAnswers, answerContent, answerTime)
assert.NoError(t, err)
}
//func TestProcessMessageToClient(t *testing.T) {
// smtpData := clients.Deps{
// SmtpHost: "connect.mailclient.bz",
// SmtpPort: "587",
// SmtpSender: "skeris@mailing.pena.digital",
// ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
// FiberClient: &fiber.Client{},
// }
//
// mailClient := clients.NewSmtpClient(smtpData)
// mailSender := senders.NewMailLeadSender(mailClient)
//
// deps := answerwc.DepsSendToClient{
// Redis: nil,
// Dal: nil,
// LeadSenders: []senders.LeadSender{mailSender},
// CustomerService: nil,
// }
//
// errChan := make(chan<- error)
//
// w := answerwc.NewSendToClient(deps, errChan)
//
// quizConfig := model.QuizConfig{
// Mailing: model.ResultInfo{
// Theme: faker.String(),
// },
// }
//
// questionsMap := map[uint64]string{
// 1: faker.String(),
// 2: faker.String(),
// }
//
// account := model.Account{
// Email: "pashamullin2001@gmail.com",
// }
//
// allAnswers := []model.ResultAnswer{
// {
// Content: `{"Image":"https://letsenhance.io/static/8f5e523ee6b2479e26ecc91b9c25261e/1015f/MainAfter.jpg","Description":"Gekon"}`,
// AnswerID: 1,
// QuestionID: 1,
// },
// {
// AnswerID: 2,
// QuestionID: 2,
// },
// }
//
// answerContent := model.ResultContent{
// Name: "Pasha",
// Phone: "+723456789",
// Email: "test@example.com",
// //Adress: "chtoto tam",
// Telegram: "@test",
// Wechat: "test_wechat",
// Viber: "+723456789",
// Vk: "test_vk",
// Skype: "test_skype",
// Whatsup: "test_whatsup",
// Messenger: "test_messenger",
// }
//
// answerTime := time.Now()
//
// err := w.ProcessMessageToClient(quizConfig, questionsMap, account, allAnswers, answerContent, answerTime)
//
// assert.NoError(t, err)
//}

47
tests/tg_sender_test.go Normal file

@ -0,0 +1,47 @@
package tests
import (
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/worker.git/senders"
"testing"
"time"
)
func Test_Tg_Sender(t *testing.T) {
tg_Sender, err := senders.NewTgSender("6712573453:AAFbioUuXf0Te73MUCqa0_h09qEQ1iQREas")
assert.NoError(t, err)
err = tg_Sender.SendLead(senders.LeadData{
To: int64(542073142),
Subject: "test_TG_Sender",
TemplateData: senders.TemplateData{
QuizConfig: model.ResultInfo{
Theme: "Taemplste Quiz",
},
AnswerContent: model.ResultContent{
Name: "Pasha",
Phone: "+723456789",
Email: "test@example.com",
//Adress: "chtoto tam",
Telegram: "@test",
Wechat: "test_wechat",
Viber: "+723456789",
Vk: "test_vk",
Skype: "test_skype",
Whatsup: "test_whatsup",
Messenger: "test_messenger",
},
AllAnswers: []model.ResultAnswer{
{AnswerID: 1, QuestionID: 1, Content: "Pasha", CreatedAt: time.Now()},
{AnswerID: 2, QuestionID: 2, Content: "From a friend", CreatedAt: time.Now()},
},
QuestionsMap: map[uint64]string{
1: "How did you hear about us?",
2: "How did you hear about us?",
},
AnswerTime: time.Now().Format("Monday, 2 January 2006 г., 15:04 UTC-07:00"),
},
})
assert.NoError(t, err)
}