Merge branch 'leadTarget' into 'dev'
Lead target See merge request backend/quiz/core!25
This commit is contained in:
commit
8f56724bac
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,3 +20,5 @@ worker/worker
|
|||||||
storer/storer
|
storer/storer
|
||||||
answerer/answerer
|
answerer/answerer
|
||||||
core
|
core
|
||||||
|
/.tdlib/
|
||||||
|
/unsetrecover.bolt
|
||||||
|
44
app/app.go
44
app/app.go
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/skeris/appInit"
|
"github.com/skeris/appInit"
|
||||||
"github.com/themakers/hlog"
|
"github.com/themakers/hlog"
|
||||||
@ -17,11 +18,13 @@ import (
|
|||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/auth"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/auth"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/telegram"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/initialize"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/initialize"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/server"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/server"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/service"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/service"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/tools"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/tools"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/workers"
|
||||||
"penahub.gitlab.yandexcloud.net/external/trashlog/wrappers/zaptrashlog"
|
"penahub.gitlab.yandexcloud.net/external/trashlog/wrappers/zaptrashlog"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -69,6 +72,10 @@ type Options struct {
|
|||||||
TrashLogHost string `env:"TRASH_LOG_HOST" default:"localhost:7113"`
|
TrashLogHost string `env:"TRASH_LOG_HOST" default:"localhost:7113"`
|
||||||
ModuleLogger string `env:"MODULE_LOGGER" default:"core-local"`
|
ModuleLogger string `env:"MODULE_LOGGER" default:"core-local"`
|
||||||
ClickHouseCred string `env:"CLICK_HOUSE_CRED" default:"tcp://10.8.0.15:9000/default?sslmode=disable"`
|
ClickHouseCred string `env:"CLICK_HOUSE_CRED" default:"tcp://10.8.0.15:9000/default?sslmode=disable"`
|
||||||
|
RedisHost string `env:"REDIS_HOST" default:"localhost:6379"`
|
||||||
|
RedisPassword string `env:"REDIS_PASSWORD" default:"admin"`
|
||||||
|
RedisDB uint64 `env:"REDIS_DB" default:"2"`
|
||||||
|
S3Prefix string `env:"S3_PREFIX"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
|
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
|
||||||
@ -143,6 +150,16 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
|
|||||||
Logger: zapLogger,
|
Logger: zapLogger,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
redisClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: options.RedisHost,
|
||||||
|
Password: options.RedisPassword,
|
||||||
|
DB: int(options.RedisDB),
|
||||||
|
})
|
||||||
|
err = redisClient.Ping(ctx).Err()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("error ping to redis db %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
clientData := privilege.Client{
|
clientData := privilege.Client{
|
||||||
URL: options.HubAdminUrl,
|
URL: options.HubAdminUrl,
|
||||||
ServiceName: options.ServiceName,
|
ServiceName: options.ServiceName,
|
||||||
@ -152,6 +169,20 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
|
|||||||
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
|
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
|
||||||
go tools.PublishPrivilege(privilegeController, 10, 5*time.Minute)
|
go tools.PublishPrivilege(privilegeController, 10, 5*time.Minute)
|
||||||
|
|
||||||
|
tgClient, err := telegram.NewTelegramClient(ctx, pgdal)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed init tg clietns: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tgWC := workers.NewTgListenerWC(workers.Deps{
|
||||||
|
BotID: int64(6712573453), // todo убрать
|
||||||
|
Redis: redisClient,
|
||||||
|
Dal: pgdal,
|
||||||
|
TgClient: tgClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
go tgWC.Start(ctx)
|
||||||
|
|
||||||
// todo подумать над реализацией всего а то пока мне кажется что немного каша получается такой предикт что через некоторое время
|
// todo подумать над реализацией всего а то пока мне кажется что немного каша получается такой предикт что через некоторое время
|
||||||
// сложно будет разобраться что есть где
|
// сложно будет разобраться что есть где
|
||||||
grpcControllers := initialize.InitRpcControllers(pgdal)
|
grpcControllers := initialize.InitRpcControllers(pgdal)
|
||||||
@ -173,11 +204,14 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
|
|||||||
app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
|
app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
|
||||||
|
|
||||||
svc := service.New(service.Deps{
|
svc := service.New(service.Deps{
|
||||||
Dal: pgdal,
|
Dal: pgdal,
|
||||||
AuthClient: authClient,
|
AuthClient: authClient,
|
||||||
Producer: producer,
|
Producer: producer,
|
||||||
ServiceName: options.ServiceName,
|
ServiceName: options.ServiceName,
|
||||||
ChDAL: chDal,
|
ChDAL: chDal,
|
||||||
|
TelegramClient: tgClient,
|
||||||
|
RedisClient: redisClient,
|
||||||
|
S3Prefix: options.S3Prefix,
|
||||||
})
|
})
|
||||||
|
|
||||||
svc.Register(app)
|
svc.Register(app)
|
||||||
|
246
clients/telegram/tg.go
Normal file
246
clients/telegram/tg.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/tdlib/client"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramClient struct {
|
||||||
|
repo *dal.DAL
|
||||||
|
TgClients map[int64]*client.Client
|
||||||
|
WaitingClients map[string]WaitingClient
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type WaitingClient struct {
|
||||||
|
PreviousReq AuthTgUserReq
|
||||||
|
Authorizer *client.ClientAuthorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTelegramClient(ctx context.Context, repo *dal.DAL) (*TelegramClient, error) {
|
||||||
|
tgClient := &TelegramClient{
|
||||||
|
repo: repo,
|
||||||
|
TgClients: make(map[int64]*client.Client),
|
||||||
|
WaitingClients: make(map[string]WaitingClient),
|
||||||
|
}
|
||||||
|
|
||||||
|
allTgAccounts, err := repo.TgRepo.GetAllTgAccounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
return tgClient, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range allTgAccounts {
|
||||||
|
if account.Status == model.ActiveTg {
|
||||||
|
authorizer := client.ClientAuthorizerr()
|
||||||
|
authorizer.TdlibParameters <- &client.SetTdlibParametersRequest{
|
||||||
|
UseTestDc: false,
|
||||||
|
DatabaseDirectory: filepath.Join(".tdlib", "database"),
|
||||||
|
FilesDirectory: filepath.Join(".tdlib", "files"),
|
||||||
|
UseFileDatabase: true,
|
||||||
|
UseChatInfoDatabase: true,
|
||||||
|
UseMessageDatabase: true,
|
||||||
|
UseSecretChats: true,
|
||||||
|
ApiId: account.ApiID,
|
||||||
|
ApiHash: account.ApiHash,
|
||||||
|
SystemLanguageCode: "en",
|
||||||
|
DeviceModel: "Server",
|
||||||
|
SystemVersion: "1.0.0",
|
||||||
|
ApplicationVersion: "1.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
|
||||||
|
NewVerbosityLevel: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tdlibClient *client.Client
|
||||||
|
var goErr error
|
||||||
|
go func() {
|
||||||
|
tdlibClient, goErr = client.NewClient(authorizer)
|
||||||
|
if goErr != nil {
|
||||||
|
fmt.Println("new client failed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("i am down")
|
||||||
|
}()
|
||||||
|
if goErr != nil {
|
||||||
|
return nil, goErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
state, ok := <-authorizer.State
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("currnet state:", state)
|
||||||
|
switch state.AuthorizationStateType() {
|
||||||
|
case client.TypeAuthorizationStateWaitPhoneNumber:
|
||||||
|
authorizer.PhoneNumber <- account.PhoneNumber
|
||||||
|
case client.TypeAuthorizationStateWaitCode:
|
||||||
|
err := tgClient.repo.TgRepo.UpdateStatusTg(ctx, account.ID, model.InactiveTg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
err := tgClient.repo.TgRepo.UpdateStatusTg(ctx, account.ID, model.InactiveTg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case client.TypeAuthorizationStateReady:
|
||||||
|
// костыль так как в либе тож костыль стоит пока там ьд обновиться будет ниловый всегда клиент
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
me, err := tdlibClient.GetMe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("Me: %s %s [%v]", me.FirstName, me.LastName, me.Usernames)
|
||||||
|
tgClient.mu.Lock()
|
||||||
|
tgClient.TgClients[account.ID] = tdlibClient
|
||||||
|
tgClient.mu.Unlock()
|
||||||
|
break
|
||||||
|
case client.TypeAuthorizationStateWaitPassword:
|
||||||
|
authorizer.Password <- account.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tgClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthTgUserReq struct {
|
||||||
|
ApiID int32 `json:"api_id"`
|
||||||
|
ApiHash string `json:"api_hash"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tg *TelegramClient) AddedToMap(data WaitingClient, id string) {
|
||||||
|
fmt.Println("AddedToMap")
|
||||||
|
tg.mu.Lock()
|
||||||
|
defer tg.mu.Unlock()
|
||||||
|
tg.WaitingClients[id] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tg *TelegramClient) GetFromMap(id string) (WaitingClient, bool) {
|
||||||
|
fmt.Println("GetFromMap")
|
||||||
|
tg.mu.Lock()
|
||||||
|
defer tg.mu.Unlock()
|
||||||
|
if data, ok := tg.WaitingClients[id]; ok {
|
||||||
|
delete(tg.WaitingClients, id)
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
return WaitingClient{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tg *TelegramClient) SaveTgAccount(appID int32, appHash string, tdLibClient *client.Client) {
|
||||||
|
account, err := tg.repo.TgRepo.SearchIDByAppIDanAppHash(context.Background(), appID, appHash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("err SaveTgAccount", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if account.Status == model.ActiveTg {
|
||||||
|
tg.mu.Lock()
|
||||||
|
defer tg.mu.Unlock()
|
||||||
|
tg.TgClients[account.ID] = tdLibClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tg *TelegramClient) CreateChannel(channelName string, botID int64) (string, int64, error) {
|
||||||
|
tg.mu.Lock()
|
||||||
|
defer tg.mu.Unlock()
|
||||||
|
if len(tg.TgClients) == 0 {
|
||||||
|
return "", 0, errors.New("no active Telegram clients")
|
||||||
|
}
|
||||||
|
var lastError error
|
||||||
|
var inviteLink string
|
||||||
|
var channelId int64
|
||||||
|
for _, activeClient := range tg.TgClients {
|
||||||
|
// todo пока не понимаю это какой то рандом? в один день бот норм находится в другой уже не находится хотя абсолютно с точки зрения тг кода этой функции и бота не менялось
|
||||||
|
_, err := activeClient.GetUser(&client.GetUserRequest{
|
||||||
|
UserId: botID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("not found this bot, make privacy off: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo нужно поймать ошибку, при которой либо бан либо медленный редим включается для того чтобы прервать
|
||||||
|
// исполнение клиента текущего аккаунта и дать задачу следующему пока поймал 1 раз и не запомнил больше не получается
|
||||||
|
channel, err := activeClient.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
|
||||||
|
Title: channelName,
|
||||||
|
IsChannel: true,
|
||||||
|
Description: "private channel",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("failed to create channel: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = activeClient.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
|
||||||
|
ChatId: channel.Id,
|
||||||
|
MemberId: &client.MessageSenderUser{UserId: botID},
|
||||||
|
Status: &client.ChatMemberStatusAdministrator{
|
||||||
|
CustomTitle: "bot",
|
||||||
|
Rights: &client.ChatAdministratorRights{
|
||||||
|
CanManageChat: true,
|
||||||
|
CanChangeInfo: true,
|
||||||
|
CanPostMessages: true,
|
||||||
|
CanEditMessages: true,
|
||||||
|
CanDeleteMessages: true,
|
||||||
|
CanInviteUsers: true,
|
||||||
|
CanRestrictMembers: true,
|
||||||
|
CanPinMessages: true,
|
||||||
|
CanManageTopics: true,
|
||||||
|
CanPromoteMembers: true,
|
||||||
|
CanManageVideoChats: true,
|
||||||
|
CanPostStories: true,
|
||||||
|
CanEditStories: true,
|
||||||
|
CanDeleteStories: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("failed to make bot admin: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteLinkResp, err := activeClient.CreateChatInviteLink(&client.CreateChatInviteLinkRequest{
|
||||||
|
ChatId: channel.Id,
|
||||||
|
Name: channelName,
|
||||||
|
ExpirationDate: 0,
|
||||||
|
MemberLimit: 0,
|
||||||
|
CreatesJoinRequest: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("failed to get invite link: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = activeClient.LeaveChat(&client.LeaveChatRequest{
|
||||||
|
ChatId: channel.Id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("failed to leave the channel: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteLink = inviteLinkResp.InviteLink
|
||||||
|
channelId = channel.Id
|
||||||
|
return inviteLink, channelId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", 0, lastError
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
version: "3"
|
||||||
services:
|
services:
|
||||||
core:
|
core:
|
||||||
hostname: squiz-core
|
hostname: squiz-core
|
||||||
@ -15,5 +16,13 @@ services:
|
|||||||
PUBLIC_KEY: $PEM_PUB_USERID
|
PUBLIC_KEY: $PEM_PUB_USERID
|
||||||
PRIVATE_KEY: $PEM_PRIV_USERID
|
PRIVATE_KEY: $PEM_PRIV_USERID
|
||||||
REDIRECT_URL: 'https://quiz.pena.digital'
|
REDIRECT_URL: 'https://quiz.pena.digital'
|
||||||
|
KAFKA_BROKERS: 10.8.0.6:9092
|
||||||
|
KAFKA_TOPIC: "mailnotifier"
|
||||||
|
GRPC_HOST: "0.0.0.0"
|
||||||
|
TRASH_LOG_HOST: "10.8.0.15:7113"
|
||||||
|
MODULE_LOGGER: "quiz-core-main"
|
||||||
|
CLICK_HOUSE_CRED: "clickhouse://10.8.0.15:9000/default?sslmode=disable"
|
||||||
|
S3_PREFIX: "https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/"
|
||||||
ports:
|
ports:
|
||||||
- 10.8.0.9:1488:1488
|
- 10.8.0.9:1488:1488
|
||||||
|
- 10.8.0.9:9000:9000
|
||||||
|
@ -15,6 +15,7 @@ services:
|
|||||||
AUTH_URL: 'http://10.8.0.6:59300/user'
|
AUTH_URL: 'http://10.8.0.6:59300/user'
|
||||||
PUBLIC_KEY: $PEM_PUB_USERID
|
PUBLIC_KEY: $PEM_PUB_USERID
|
||||||
PRIVATE_KEY: $PEM_PRIV_USERID
|
PRIVATE_KEY: $PEM_PRIV_USERID
|
||||||
|
REDIRECT_URL: 'https://quiz.pena.digital'
|
||||||
KAFKA_BROKERS: 10.8.0.6:9092
|
KAFKA_BROKERS: 10.8.0.6:9092
|
||||||
KAFKA_TOPIC: "mailnotifier"
|
KAFKA_TOPIC: "mailnotifier"
|
||||||
GRPC_HOST: "0.0.0.0"
|
GRPC_HOST: "0.0.0.0"
|
||||||
|
17
go.mod
17
go.mod
@ -1,16 +1,16 @@
|
|||||||
module penahub.gitlab.yandexcloud.net/backend/quiz/core
|
module penahub.gitlab.yandexcloud.net/backend/quiz/core
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.4
|
||||||
|
|
||||||
toolchain go1.22.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/gofiber/fiber/v2 v2.52.4
|
github.com/gofiber/fiber/v2 v2.52.4
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/pioz/faker v1.7.3
|
github.com/pioz/faker v1.7.3
|
||||||
|
github.com/rs/xid v1.5.0
|
||||||
github.com/skeris/appInit v1.0.2
|
github.com/skeris/appInit v1.0.2
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
|
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
|
||||||
@ -20,10 +20,10 @@ require (
|
|||||||
google.golang.org/grpc v1.64.0
|
google.golang.org/grpc v1.64.0
|
||||||
google.golang.org/protobuf v1.34.2
|
google.golang.org/protobuf v1.34.2
|
||||||
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c
|
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240703125409-25e0fe5d6051
|
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240711133242-0b8534fae5b2
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990
|
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990
|
||||||
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c
|
penahub.gitlab.yandexcloud.net/backend/tdlib v0.0.0-20240701075856-1731684c936f
|
||||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5
|
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.6-0.20240827173635-78ce9878c387
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -34,7 +34,6 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@ -52,14 +51,14 @@ require (
|
|||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.3 // indirect
|
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||||
github.com/twmb/franz-go/pkg/kmsg v1.8.0 // indirect
|
github.com/twmb/franz-go/pkg/kmsg v1.8.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.53.0 // indirect
|
github.com/valyala/fasthttp v1.53.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
||||||
go.etcd.io/bbolt v1.3.10 // indirect
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.24.0 // indirect
|
golang.org/x/crypto v0.24.0 // indirect
|
||||||
golang.org/x/net v0.26.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
|
19
go.sum
19
go.sum
@ -162,8 +162,8 @@ github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNh
|
|||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
@ -210,12 +210,11 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -283,11 +282,11 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
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 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/penahub_common v0.0.0-20240607202348-efe5f2bf3e8c/go.mod h1:+bPxq2wfW5S1gd+83vZYmHm33AE7nEBfznWS8AM1TKE=
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240630122905-2747a8c00d81 h1:HSEcPZ8PVOrhj6d7/7MjHibxu/+3KXUeFbd/JpZ//bI=
|
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240711133242-0b8534fae5b2 h1:0t6pQHJvA3jMeBB3FPUmA+hw8rWlksTah8KJEtf2KD8=
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240630122905-2747a8c00d81/go.mod h1:nfZkoj8MCYaoP+xiPeUn5D0lIzinUr1qDkNfX0ng9rk=
|
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240711133242-0b8534fae5b2/go.mod h1:uOuosXduBzd2WbLH6TDZO7ME7ZextulA662oZ6OsoB0=
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990 h1:jiO8GWO+3sCnDAV8/NAV8tQIUwae/I6/xiDilW7zf0o=
|
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990 h1:jiO8GWO+3sCnDAV8/NAV8tQIUwae/I6/xiDilW7zf0o=
|
||||||
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990/go.mod h1:zswBuTwmEsFHBVRu1nkG3/Fwylk5Vcm8OUm9iWxccSE=
|
penahub.gitlab.yandexcloud.net/backend/quiz/worker.git v0.0.0-20240421230341-0e086fcbb990/go.mod h1:zswBuTwmEsFHBVRu1nkG3/Fwylk5Vcm8OUm9iWxccSE=
|
||||||
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c h1:imtXaIVscs8it6SfAmDxjNxqQSF44GgCTl1N6JT6unA=
|
penahub.gitlab.yandexcloud.net/backend/tdlib v0.0.0-20240701075856-1731684c936f h1:Qli89wgu0T7nG4VECXZOZ40fjE/hVVfxF3hTaSYS008=
|
||||||
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c/go.mod h1:i7M72RIpkSjcQtHID6KKj9RT/EYZ1rxS6tIPKWa/BSY=
|
penahub.gitlab.yandexcloud.net/backend/tdlib v0.0.0-20240701075856-1731684c936f/go.mod h1:AkE19hcbDwB7hoEASwImm7rUI+cK/8jMVJaTvMK4F+c=
|
||||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5 h1:amsK0bkSJxBisk334aFo5ZmVPvN1dBT0Sv5j3V5IsT8=
|
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.6-0.20240827173635-78ce9878c387 h1:G+GIhkkvUsM9No2rf2D4kvQ2ExTw9KxlA8vsSnC0ywU=
|
||||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5/go.mod h1:J8kQNEP4bL7ZNKHxuT4tfe6a3FHyovpAPkyytN4qllc=
|
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.6-0.20240827173635-78ce9878c387/go.mod h1:30nezjpGpZuNThbQOCULIfa79RoJ5sray593jhfVP/Q=
|
||||||
|
364
openapi.yaml
364
openapi.yaml
@ -784,8 +784,72 @@ components:
|
|||||||
Deleted:
|
Deleted:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: удален?
|
description: удален?
|
||||||
|
LeadTarget:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
AccountID:
|
||||||
|
type: string
|
||||||
|
Type:
|
||||||
|
type: string
|
||||||
|
QuizID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
Target:
|
||||||
|
type: string
|
||||||
|
InviteLink:
|
||||||
|
type: string
|
||||||
|
Deleted:
|
||||||
|
type: boolean
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
TgAccountStatus:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- active
|
||||||
|
- inactive
|
||||||
|
- ban
|
||||||
|
TgAccount:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
ApiID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
ApiHash:
|
||||||
|
type: string
|
||||||
|
PhoneNumber:
|
||||||
|
type: string
|
||||||
|
Password:
|
||||||
|
type: string
|
||||||
|
Status:
|
||||||
|
$ref: '#/components/schemas/TgAccountStatus'
|
||||||
|
Deleted:
|
||||||
|
type: boolean
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
AuthTgUserReq:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- ApiID
|
||||||
|
- ApiHash
|
||||||
|
- PhoneNumber
|
||||||
|
- Password
|
||||||
|
properties:
|
||||||
|
ApiID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
ApiHash:
|
||||||
|
type: string
|
||||||
|
PhoneNumber:
|
||||||
|
type: string
|
||||||
|
Password:
|
||||||
|
type: string
|
||||||
paths:
|
paths:
|
||||||
/liveness:
|
/liveness:
|
||||||
get:
|
get:
|
||||||
@ -1558,6 +1622,211 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
/account/leadtarget:
|
||||||
|
post:
|
||||||
|
description: Метод для добавления целевых мест, куда будут посылаться заявки клиенту.
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- quizID
|
||||||
|
- target
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Тип цели (mail, telegram, whatsapp).
|
||||||
|
enum:
|
||||||
|
- mail
|
||||||
|
- telegram
|
||||||
|
- whatsapp
|
||||||
|
quizID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: ID квиза, к которому прикреплено это правило (приоритет). Передавать как 0, если правило не прикрепляется к квизу и является общим.
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
description: Адресат, куда конкретно слать (для mail - email, для telegram - ID канала, передавать не нужно канал сам создаться, для whatsapp - номер телефона, наверное).
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: имя например для тг канала
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило добавлено если тип mail о сразу добавляется если тг то будет добавленно в воркере если ватсап пока тодо
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
put:
|
||||||
|
description: Метод для обновления целевого места, куда будут посылаться заявки клиенту.
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- target
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: id этой самой цели, primary key.
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
description: Адресат, куда конкретно слать (для mail - email, для telegram - ID чата, для whatsapp - номер телефона, наверное).
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило обновлено
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'404':
|
||||||
|
description: NotFound, такого не существует
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
/account/leadtarget/{id}:
|
||||||
|
delete:
|
||||||
|
description: удаление правила по id, primary key
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило удалено
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
/account/leadtarget/{quizID}:
|
||||||
|
get:
|
||||||
|
description: получение правила по quizID, так же стоит передавать 0 если правило не было привязано к определенному квизу, возвращает массив
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвила получены
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'404':
|
||||||
|
description: NotFound, такого не существует
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
/statistics/:quizID/pipelines:
|
/statistics/:quizID/pipelines:
|
||||||
get:
|
get:
|
||||||
description: получение статистики по векторам прохождения респондентами опроса с ветвлением и без, на выход отдается мапа с ключем последний вопрос и массивом точек "точек прохождения пользователем вопросов" грубо говоря массив с векторами как двигался респондент по возможным путям, в этом массиве question id и count прошедших сессий через него
|
description: получение статистики по векторам прохождения респондентами опроса с ветвлением и без, на выход отдается мапа с ключем последний вопрос и массивом точек "точек прохождения пользователем вопросов" грубо говоря массив с векторами как двигался респондент по возможным путям, в этом массиве question id и count прошедших сессий через него
|
||||||
@ -1581,3 +1850,94 @@ paths:
|
|||||||
description: Bad Request
|
description: Bad Request
|
||||||
'500':
|
'500':
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
|
/telegram/pool:
|
||||||
|
get:
|
||||||
|
description: возвращает все неудаленные аккаунты тг, активные, не активные и баны, тело пустое
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: успех
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/TgAccount'
|
||||||
|
/telegram/create:
|
||||||
|
post:
|
||||||
|
description: метод для автторизации сервера в тг аккаунте
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthTgUserReq'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: возвращает подпись, которая является идентификатором текущей сессии авторизации нужно для метода отправки кода
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
example: b7gh83j2k4l0
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'409':
|
||||||
|
description: аккаунт уже существует и активен
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
||||||
|
/telegram/{id}:
|
||||||
|
delete:
|
||||||
|
description: метод мягкого удаления аккаунта по id primary key
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
description: id primary key
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: успех
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
||||||
|
|
||||||
|
/telegram/setCode:
|
||||||
|
post:
|
||||||
|
description: метод для отправки кода авторизации, который пришел от телеграмма
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- signature
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: возвращает id primary авторизованного аккаунта
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
'204':
|
||||||
|
description: state канал закрылся до того как перешел в состояние логина или отказа от логина, возможно стоит другой статус указывать или как то побороть эту беду
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'403':
|
||||||
|
description: что то пошло не так связано с тг
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
@ -2,7 +2,9 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw"
|
"penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
|
||||||
@ -10,6 +12,7 @@ import (
|
|||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -91,7 +94,6 @@ func (s *Service) createAccount(ctx *fiber.Ctx) error {
|
|||||||
newAccount := model.Account{
|
newAccount := model.Account{
|
||||||
UserID: accountID,
|
UserID: accountID,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
Email: email,
|
|
||||||
Deleted: false,
|
Deleted: false,
|
||||||
Privileges: map[string]model.ShortPrivilege{
|
Privileges: map[string]model.ShortPrivilege{
|
||||||
"quizUnlimTime": {
|
"quizUnlimTime": {
|
||||||
@ -107,6 +109,15 @@ func (s *Service) createAccount(ctx *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
_, err = s.dal.AccountRepo.PostLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
AccountID: accountID,
|
||||||
|
Target: email,
|
||||||
|
Type: model.LeadTargetEmail,
|
||||||
|
QuizID: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
hlogger.Emit(models.InfoAccountCreated{
|
hlogger.Emit(models.InfoAccountCreated{
|
||||||
CtxUserID: accountID,
|
CtxUserID: accountID,
|
||||||
@ -236,3 +247,145 @@ func (s *Service) ManualDone(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
return ctx.SendStatus(fiber.StatusOK)
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) PostLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
QuizID int32 `json:"quizID"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
//accountID := "64f2cd7a7047f28fdabf6d9e"
|
||||||
|
|
||||||
|
if _, ok := model.ValidLeadTargetTypes[req.Type]; !ok {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type == "" || (req.Target == "" && req.Type != string(model.LeadTargetTg)) {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Type and Target don't be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Type {
|
||||||
|
case "mail":
|
||||||
|
_, err := s.dal.AccountRepo.PostLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
AccountID: accountID,
|
||||||
|
Target: req.Target,
|
||||||
|
Type: model.LeadTargetType(req.Type),
|
||||||
|
QuizID: req.QuizID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
case "telegram":
|
||||||
|
targets, err := s.dal.AccountRepo.GetLeadTarget(ctx.Context(), accountID, req.QuizID)
|
||||||
|
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
if !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.Type == model.LeadTargetTg {
|
||||||
|
return ctx.Status(fiber.StatusAlreadyReported).SendString("LeadTarget for this quiz already exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task := model.TgRedisTask{
|
||||||
|
Name: req.Name,
|
||||||
|
QuizID: req.QuizID,
|
||||||
|
AccountID: accountID,
|
||||||
|
}
|
||||||
|
|
||||||
|
taskKey := fmt.Sprintf("telegram_task:%d", time.Now().UnixNano())
|
||||||
|
taskData, err := json.Marshal(task)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Set(ctx.Context(), taskKey, taskData, 0).Err(); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
case "whatsapp":
|
||||||
|
return ctx.Status(fiber.StatusOK).SendString("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
leadIDStr := ctx.Params("id")
|
||||||
|
leadID, err := strconv.ParseInt(leadIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid lead ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.dal.AccountRepo.DeleteLeadTarget(ctx.Context(), leadID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
quizIDStr := ctx.Params("quizID")
|
||||||
|
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.dal.AccountRepo.GetLeadTarget(ctx.Context(), accountID, int32(quizID))
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("this lead target not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID == 0 || req.Target == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("ID and Target don't be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.dal.AccountRepo.UpdateLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
ID: req.ID,
|
||||||
|
Target: req.Target,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("this lead target not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(result)
|
||||||
|
}
|
||||||
|
@ -63,6 +63,7 @@ func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
|
|||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
}
|
}
|
||||||
|
|
||||||
questionID, err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
questionID, err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*pq.Error); ok {
|
if e, ok := err.(*pq.Error); ok {
|
||||||
@ -75,7 +76,7 @@ func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
hlogger.Emit(models.InfoQuestionCreate{
|
hlogger.Emit(models.InfoQuestionCreate{
|
||||||
CtxUserID: accountID,
|
CtxUserID: accountID,
|
||||||
CtxIDInt: int64(req.QuizId),
|
CtxIDInt: int64(req.QuizId),
|
||||||
CtxQuestionID: int64(questionID),
|
CtxQuestionID: int64(questionID),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -312,7 +313,7 @@ func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
hlogger.Emit(models.InfoQuestionDelete{
|
hlogger.Emit(models.InfoQuestionDelete{
|
||||||
CtxUserID: accountID,
|
CtxUserID: accountID,
|
||||||
CtxIDInt: int64(deleted.QuizId),
|
CtxIDInt: int64(deleted.QuizId),
|
||||||
CtxQuestionID: int64(deleted.Id),
|
CtxQuestionID: int64(deleted.Id),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateQuizReq struct {
|
type CreateQuizReq struct {
|
||||||
@ -101,7 +102,7 @@ func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
hlogger.Emit(models.InfoQuizCreated{
|
hlogger.Emit(models.InfoQuizCreated{
|
||||||
CtxUserID: accountId,
|
CtxUserID: accountId,
|
||||||
CtxIDInt: int64(quizID),
|
CtxIDInt: int64(quizID),
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusCreated).JSON(record)
|
return ctx.Status(fiber.StatusCreated).JSON(record)
|
||||||
@ -314,13 +315,13 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
|
|||||||
if req.Status == model.StatusStart {
|
if req.Status == model.StatusStart {
|
||||||
hlogger.Emit(models.InfoQuizPublish{
|
hlogger.Emit(models.InfoQuizPublish{
|
||||||
CtxUserID: accountId,
|
CtxUserID: accountId,
|
||||||
CtxIDInt: int64(quiz.Id),
|
CtxIDInt: int64(quiz.Id),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if req.Status == model.StatusStop {
|
if req.Status == model.StatusStop {
|
||||||
hlogger.Emit(models.InfoQuizStop{
|
hlogger.Emit(models.InfoQuizStop{
|
||||||
CtxUserID: accountId,
|
CtxUserID: accountId,
|
||||||
CtxIDInt: int64(quiz.Id),
|
CtxIDInt: int64(quiz.Id),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +429,7 @@ func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
hlogger.Emit(models.InfoQuizDelete{
|
hlogger.Emit(models.InfoQuizDelete{
|
||||||
CtxUserID: accountId,
|
CtxUserID: accountId,
|
||||||
CtxIDInt: int64(req.Id),
|
CtxIDInt: int64(req.Id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx.JSON(DeactivateResp{
|
return ctx.JSON(DeactivateResp{
|
||||||
@ -505,6 +506,7 @@ func (s *Service) TemplateCopy(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
qizID, err := s.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
|
qizID, err := s.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("TEMPLERR", err)
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,6 +159,11 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quiz, err := s.dal.QuizRepo.GetQuizById(ctx.Context(), accountID, quizID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get quiz")
|
||||||
|
}
|
||||||
|
|
||||||
questions, err := s.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
|
questions, err := s.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
|
||||||
@ -177,7 +182,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
if err := tools.WriteDataToExcel(buffer, questions, answers); err != nil {
|
if err := tools.WriteDataToExcel(buffer, questions, answers, s.s3Prefix + quiz.Qid + "/"); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,47 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/brokers"
|
||||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/auth"
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/auth"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/telegram"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is an entity for http requests handling
|
// Service is an entity for http requests handling
|
||||||
type Service struct {
|
type Service struct {
|
||||||
dal *dal.DAL
|
dal *dal.DAL
|
||||||
authClient *auth.AuthClient
|
authClient *auth.AuthClient
|
||||||
producer *brokers.Producer
|
producer *brokers.Producer
|
||||||
serviceName string
|
serviceName string
|
||||||
chDAL *dal.ClickHouseDAL
|
chDAL *dal.ClickHouseDAL
|
||||||
|
telegramClient *telegram.TelegramClient
|
||||||
|
redisClient *redis.Client
|
||||||
|
s3Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
Dal *dal.DAL
|
Dal *dal.DAL
|
||||||
AuthClient *auth.AuthClient
|
AuthClient *auth.AuthClient
|
||||||
Producer *brokers.Producer
|
Producer *brokers.Producer
|
||||||
ServiceName string
|
ServiceName string
|
||||||
ChDAL *dal.ClickHouseDAL
|
ChDAL *dal.ClickHouseDAL
|
||||||
|
TelegramClient *telegram.TelegramClient
|
||||||
|
RedisClient *redis.Client
|
||||||
|
S3Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(deps Deps) *Service {
|
func New(deps Deps) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
dal: deps.Dal,
|
dal: deps.Dal,
|
||||||
authClient: deps.AuthClient,
|
authClient: deps.AuthClient,
|
||||||
producer: deps.Producer,
|
producer: deps.Producer,
|
||||||
serviceName: deps.ServiceName,
|
serviceName: deps.ServiceName,
|
||||||
chDAL: deps.ChDAL,
|
chDAL: deps.ChDAL,
|
||||||
|
telegramClient: deps.TelegramClient,
|
||||||
|
redisClient: deps.RedisClient,
|
||||||
|
s3Prefix: deps.S3Prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +74,10 @@ func (s *Service) Register(app *fiber.App) {
|
|||||||
app.Get("/privilege/:userId", s.getPrivilegeByUserID)
|
app.Get("/privilege/:userId", s.getPrivilegeByUserID)
|
||||||
app.Delete("/account/:userId", s.deleteAccountByUserID)
|
app.Delete("/account/:userId", s.deleteAccountByUserID)
|
||||||
app.Post("/account/manualdone", s.ManualDone)
|
app.Post("/account/manualdone", s.ManualDone)
|
||||||
|
app.Post("/account/leadtarget", s.PostLeadTarget)
|
||||||
|
app.Delete("/account/leadtarget/:id", s.DeleteLeadTarget)
|
||||||
|
app.Get("/account/leadtarget/:quizID", s.GetLeadTarget)
|
||||||
|
app.Put("/account/leadtarget", s.UpdateLeadTarget)
|
||||||
|
|
||||||
// result handlers
|
// result handlers
|
||||||
app.Post("/results/getResults/:quizId", s.GetResultsByQuizID)
|
app.Post("/results/getResults/:quizId", s.GetResultsByQuizID)
|
||||||
@ -77,4 +92,10 @@ func (s *Service) Register(app *fiber.App) {
|
|||||||
app.Post("/statistic/:quizID/questions", s.GetQuestionsStatistics)
|
app.Post("/statistic/:quizID/questions", s.GetQuestionsStatistics)
|
||||||
app.Post("/statistic", s.AllServiceStatistics)
|
app.Post("/statistic", s.AllServiceStatistics)
|
||||||
app.Get("/statistics/:quizID/pipelines", s.GetPipelinesStatistics)
|
app.Get("/statistics/:quizID/pipelines", s.GetPipelinesStatistics)
|
||||||
|
|
||||||
|
//telegram handlers
|
||||||
|
app.Get("/telegram/pool", s.GetPoolTgAccounts)
|
||||||
|
app.Post("/telegram/create", s.AddingTgAccount)
|
||||||
|
app.Delete("/telegram/:id", s.DeleteTgAccountByID)
|
||||||
|
app.Post("/telegram/setCode", s.SettingTgCode)
|
||||||
}
|
}
|
||||||
|
172
service/telegram_svc.go
Normal file
172
service/telegram_svc.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
"path/filepath"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/quiz/core/clients/telegram"
|
||||||
|
"penahub.gitlab.yandexcloud.net/backend/tdlib/client"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPoolTgAccounts(ctx *fiber.Ctx) error {
|
||||||
|
allAccounts, err := s.dal.TgRepo.GetAllTgAccounts(ctx.Context())
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(allAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AddingTgAccount(ctx *fiber.Ctx) error {
|
||||||
|
var req telegram.AuthTgUserReq
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
if req.ApiID == 0 || req.ApiHash == "" || req.Password == "" || req.PhoneNumber == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("empty required fields")
|
||||||
|
}
|
||||||
|
allAccounts, err := s.dal.TgRepo.GetAllTgAccounts(ctx.Context())
|
||||||
|
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
if !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
for _, account := range allAccounts {
|
||||||
|
if account.ApiID == req.ApiID && account.ApiHash == req.ApiHash && account.Status == model.ActiveTg {
|
||||||
|
return ctx.Status(fiber.StatusConflict).SendString("this account already exist and active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authorizer := client.ClientAuthorizerr()
|
||||||
|
authorizer.TdlibParameters <- &client.SetTdlibParametersRequest{
|
||||||
|
UseTestDc: false,
|
||||||
|
DatabaseDirectory: filepath.Join(".tdlib", "database"),
|
||||||
|
FilesDirectory: filepath.Join(".tdlib", "files"),
|
||||||
|
UseFileDatabase: true,
|
||||||
|
UseChatInfoDatabase: true,
|
||||||
|
UseMessageDatabase: true,
|
||||||
|
UseSecretChats: true,
|
||||||
|
ApiId: req.ApiID,
|
||||||
|
ApiHash: req.ApiHash,
|
||||||
|
SystemLanguageCode: "en",
|
||||||
|
DeviceModel: "Server",
|
||||||
|
SystemVersion: "1.0.0",
|
||||||
|
ApplicationVersion: "1.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
|
||||||
|
NewVerbosityLevel: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var tdlibClient *client.Client
|
||||||
|
// завершается уже в другом контроллере
|
||||||
|
var goErr error
|
||||||
|
// todo ужно продумать завершение горутины если код вставлять не пошли
|
||||||
|
go func() {
|
||||||
|
tdlibClient, goErr = client.NewClient(authorizer)
|
||||||
|
if goErr != nil {
|
||||||
|
fmt.Println("new client failed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.telegramClient.SaveTgAccount(req.ApiID, req.ApiHash, tdlibClient)
|
||||||
|
fmt.Println("i am down")
|
||||||
|
}()
|
||||||
|
if goErr != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(goErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
state, ok := <-authorizer.State
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusOK).SendString("state chan is close auth maybe ok")
|
||||||
|
}
|
||||||
|
fmt.Println("currnet state:", state)
|
||||||
|
switch state.AuthorizationStateType() {
|
||||||
|
case client.TypeAuthorizationStateWaitPhoneNumber:
|
||||||
|
authorizer.PhoneNumber <- req.PhoneNumber
|
||||||
|
case client.TypeAuthorizationStateWaitCode:
|
||||||
|
signature := xid.New()
|
||||||
|
s.telegramClient.AddedToMap(telegram.WaitingClient{
|
||||||
|
PreviousReq: req,
|
||||||
|
Authorizer: authorizer,
|
||||||
|
}, signature.String())
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"signature": signature.String()})
|
||||||
|
|
||||||
|
case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
return ctx.Status(fiber.StatusForbidden).SendString(fmt.Sprintf("auth failed, last state is %s", state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SettingTgCode(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code == "" || req.Signature == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("empty required fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := s.telegramClient.GetFromMap(req.Signature)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid id, don't have data")
|
||||||
|
}
|
||||||
|
data.Authorizer.Code <- req.Code
|
||||||
|
for {
|
||||||
|
state, ok := <-data.Authorizer.State
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusNoContent).SendString("state chan is close auth maybe ok")
|
||||||
|
}
|
||||||
|
fmt.Println("currnet state:", state)
|
||||||
|
switch state.AuthorizationStateType() {
|
||||||
|
case client.TypeAuthorizationStateReady:
|
||||||
|
id, err := s.dal.TgRepo.CreateTgAccount(ctx.Context(), model.TgAccount{
|
||||||
|
ApiID: data.PreviousReq.ApiID,
|
||||||
|
ApiHash: data.PreviousReq.ApiHash,
|
||||||
|
PhoneNumber: data.PreviousReq.PhoneNumber,
|
||||||
|
Status: model.ActiveTg,
|
||||||
|
Password: data.PreviousReq.Password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"id": id})
|
||||||
|
case client.TypeAuthorizationStateWaitPassword:
|
||||||
|
data.Authorizer.Password <- data.PreviousReq.Password
|
||||||
|
case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
return ctx.Status(fiber.StatusForbidden).SendString(fmt.Sprintf("auth failed, last state is %s", state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteTgAccountByID(ctx *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseInt(ctx.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("invalid id format")
|
||||||
|
}
|
||||||
|
err = s.dal.TgRepo.SoftDeleteTgAccount(ctx.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
420
tools/tools.go
420
tools/tools.go
@ -1,6 +1,7 @@
|
|||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
@ -27,7 +28,7 @@ const (
|
|||||||
bucketAnswers = "squizanswer"
|
bucketAnswers = "squizanswer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer) error {
|
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) error {
|
||||||
file := excelize.NewFile()
|
file := excelize.NewFile()
|
||||||
sheet := "Sheet1"
|
sheet := "Sheet1"
|
||||||
|
|
||||||
@ -40,6 +41,35 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
|||||||
return questions[i].Page < questions[j].Page
|
return questions[i].Page < questions[j].Page
|
||||||
})
|
})
|
||||||
|
|
||||||
|
headers, mapQueRes := prepareHeaders(questions)
|
||||||
|
for col, header := range headers {
|
||||||
|
cell := ToAlphaString(col+1) + "1"
|
||||||
|
if err := file.SetCellValue(sheet, cell, header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(answers, func(i, j int) bool {
|
||||||
|
return answers[i].QuestionId < answers[j].QuestionId
|
||||||
|
})
|
||||||
|
standart, results := categorizeAnswers(answers)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
row := 2
|
||||||
|
for session := range results {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(session string, response []model.Answer, row int) {
|
||||||
|
defer wg.Done()
|
||||||
|
processSession(file, sheet, session, s3Prefix, response, results, questions, mapQueRes, headers, row)
|
||||||
|
}(session, standart[session], row)
|
||||||
|
row++
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return file.Write(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHeaders(questions []model.Question) ([]string, map[uint64]string) {
|
||||||
headers := []string{"Данные респондента"}
|
headers := []string{"Данные респондента"}
|
||||||
mapQueRes := make(map[uint64]string)
|
mapQueRes := make(map[uint64]string)
|
||||||
|
|
||||||
@ -52,27 +82,14 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = append(headers, "Результат")
|
headers = append(headers, "Результат")
|
||||||
|
return headers, mapQueRes
|
||||||
|
}
|
||||||
|
|
||||||
for col, header := range headers {
|
func categorizeAnswers(answers []model.Answer) (map[string][]model.Answer, map[string]model.Answer) {
|
||||||
cell := ToAlphaString(col+1) + "1"
|
|
||||||
if err := file.SetCellValue(sheet, cell, header); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(answers, func(i, j int) bool {
|
|
||||||
return answers[i].QuestionId < answers[j].QuestionId
|
|
||||||
})
|
|
||||||
|
|
||||||
// мапа для хранения обычных ответов респондентов
|
|
||||||
standart := make(map[string][]model.Answer)
|
standart := make(map[string][]model.Answer)
|
||||||
|
|
||||||
// мапа для хранения данных респондентов
|
|
||||||
results := make(map[string]model.Answer)
|
results := make(map[string]model.Answer)
|
||||||
|
|
||||||
// заполняем мапу ответами и данными респондентов
|
|
||||||
for _, answer := range answers {
|
for _, answer := range answers {
|
||||||
if answer.Result {
|
if answer.Result {
|
||||||
results[answer.Session] = answer
|
results[answer.Session] = answer
|
||||||
@ -80,17 +97,96 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
|||||||
standart[answer.Session] = append(standart[answer.Session], answer)
|
standart[answer.Session] = append(standart[answer.Session], answer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return standart, results
|
||||||
|
}
|
||||||
|
|
||||||
processSession := func(session string, response []model.Answer, row int) {
|
func processSession(file *excelize.File, sheet, session, s3Prefix string, response []model.Answer, results map[string]model.Answer, questions []model.Question, mapQueRes map[uint64]string, headers []string, row int) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
fmt.Println("Recovered from panic:", r)
|
fmt.Println("Recovered from panic:", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
count := 2
|
||||||
|
for _, q := range questions {
|
||||||
|
if !q.Deleted && q.Type != model.TypeResult {
|
||||||
|
cell := ToAlphaString(count) + strconv.Itoa(row)
|
||||||
|
index := binarySearch(response, q.Id)
|
||||||
|
if index != -1 {
|
||||||
|
handleAnswer(file, sheet, cell, s3Prefix, response[index], q, count, row)
|
||||||
|
} else {
|
||||||
|
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
||||||
|
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil {
|
func handleAnswer(file *excelize.File, sheet, cell, s3Prefix string, answer model.Answer, question model.Question, count, row int) {
|
||||||
|
tipe := FileSearch(answer.Content)
|
||||||
|
noAccept := make(map[string]struct{})
|
||||||
|
todoMap := make(map[string]string)
|
||||||
|
|
||||||
|
if tipe != "Text" && (question.Type == model.TypeImages || question.Type == model.TypeVarImages) {
|
||||||
|
handleImage(file, sheet, cell, answer.Content, count, row, noAccept, todoMap, question.Title)
|
||||||
|
} else if question.Type == model.TypeFile {
|
||||||
|
handleFile(file, sheet, cell, answer.Content, s3Prefix, noAccept)
|
||||||
|
} else {
|
||||||
|
todoMap[answer.Content] = cell
|
||||||
|
}
|
||||||
|
|
||||||
|
for cnt, cel := range todoMap {
|
||||||
|
if _, ok := noAccept[cnt]; !ok {
|
||||||
|
cntArr := strings.Split(cnt, "`,`")
|
||||||
|
resultCnt := cnt
|
||||||
|
if len(cntArr) > 1 {
|
||||||
|
resultCnt = strings.Join(cntArr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[0] == '`' && resultCnt[len(resultCnt)-1] == '`' {
|
||||||
|
resultCnt = resultCnt[1 : len(resultCnt)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[0] == '`' {
|
||||||
|
resultCnt = resultCnt[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[len(resultCnt)-1] == '`' {
|
||||||
|
resultCnt = resultCnt[:len(resultCnt)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cel, resultCnt); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleImage(file *excelize.File, sheet, cell, content string, count, row int, noAccept map[string]struct{}, todoMap map[string]string, questionTitle string) {
|
||||||
|
multiImgArr := strings.Split(content, "`,`")
|
||||||
|
if len(multiImgArr) > 1 {
|
||||||
|
var descriptions []string
|
||||||
|
mediaSheet := "Media"
|
||||||
|
flag, err := file.GetSheetIndex(mediaSheet)
|
||||||
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
if flag == -1 {
|
||||||
|
_, _ = file.NewSheet(mediaSheet)
|
||||||
|
err = file.SetCellValue(mediaSheet, "A1", "Вопрос")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
count := 2
|
count := 2
|
||||||
for _, q := range questions {
|
for _, q := range questions {
|
||||||
if !q.Deleted && q.Type != model.TypeResult {
|
if !q.Deleted && q.Type != model.TypeResult {
|
||||||
@ -152,38 +248,134 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
mediaRow := row
|
||||||
cell := ToAlphaString(count) + strconv.Itoa(row)
|
for i, imgContent := range multiImgArr {
|
||||||
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
if i == 0 && len(imgContent) > 1 && imgContent[0] == '`' {
|
||||||
|
imgContent = imgContent[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(multiImgArr)-1 && len(imgContent) > 1 && imgContent[len(imgContent)-1] == '`' {
|
||||||
|
imgContent = imgContent[:len(imgContent)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var res model.ImageContent
|
||||||
|
err := json.Unmarshal([]byte(imgContent), &res)
|
||||||
|
if err != nil {
|
||||||
|
res.Image = imgContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// чек на пустой дескрипшен, есмли пустой то отмечаем как вариант ответа номер по i
|
||||||
|
if res.Description != "" {
|
||||||
|
descriptions = append(descriptions, res.Description)
|
||||||
|
} else {
|
||||||
|
descriptions = append(descriptions, fmt.Sprintf("Вариант ответа №%d", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
urle := ExtractImageURL(res.Image)
|
||||||
|
urlData := strings.Split(urle, " ")
|
||||||
|
if len(urlData) == 1 {
|
||||||
|
u, err := url.Parse(urle)
|
||||||
|
if err == nil && u.Scheme != "" && u.Host != "" {
|
||||||
|
picture, err := downloadImage(urle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = file.SetCellValue(mediaSheet, "A"+strconv.Itoa(mediaRow), questionTitle)
|
||||||
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
col := ToAlphaString(i + 2)
|
||||||
|
err = file.SetColWidth(mediaSheet, col, col, 50)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetRowHeight(mediaSheet, mediaRow, 150)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.AddPictureFromBytes(mediaSheet, col+strconv.Itoa(mediaRow), picture); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
}
|
}
|
||||||
count++
|
} else {
|
||||||
|
todoMap[imgContent] = cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descriptionsStr := strings.Join(descriptions, "\n")
|
||||||
|
linkText := fmt.Sprintf("%s\n Перейти в приложение %s!A%d", descriptionsStr, mediaSheet, mediaRow)
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cell, linkText); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
//if err := file.SetCellHyperLink(sheet, cell, fmt.Sprintf("%s!A%d", mediaSheet, mediaRow), "Location", excelize.HyperlinkOpts{
|
||||||
|
// Display: &linkText,
|
||||||
|
//}); err != nil {
|
||||||
|
// fmt.Println(err.Error())
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
} else {
|
||||||
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
if len(content) > 1 && content[0] == '`' && content[len(content)-1] == '`' {
|
||||||
fmt.Println(err.Error())
|
content = content[1 : len(content)-1]
|
||||||
|
}
|
||||||
|
var res model.ImageContent
|
||||||
|
err := json.Unmarshal([]byte(content), &res)
|
||||||
|
if err != nil {
|
||||||
|
res.Image = content
|
||||||
|
}
|
||||||
|
urle := ExtractImageURL(res.Image)
|
||||||
|
urlData := strings.Split(urle, " ")
|
||||||
|
if len(urlData) == 1 {
|
||||||
|
u, err := url.Parse(urle)
|
||||||
|
if err == nil && u.Scheme != "" && u.Host != "" {
|
||||||
|
picture, err := downloadImage(urle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetRowHeight(sheet, row, 150)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
row := 2
|
func handleFile(file *excelize.File, sheet, cell, content, s3Prefix string, noAccept map[string]struct{}) {
|
||||||
var wg sync.WaitGroup
|
urle := content
|
||||||
for session, _ := range results {
|
if urle != "" && !strings.HasPrefix(urle, "https") {
|
||||||
wg.Add(1)
|
urle = s3Prefix + urle
|
||||||
go func(session string, response []model.Answer, row int) {
|
|
||||||
defer wg.Done()
|
|
||||||
processSession(session, standart[session], row)
|
|
||||||
}(session, standart[session], row)
|
|
||||||
row++
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := file.Write(buffer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
fmt.Println("ORRRRR", urle, s3Prefix)
|
||||||
|
display, tooltip := urle, urle
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cell, urle); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{
|
||||||
|
Display: &display,
|
||||||
|
Tooltip: &tooltip,
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func binarySearch(answers []model.Answer, questionID uint64) int {
|
func binarySearch(answers []model.Answer, questionID uint64) int {
|
||||||
@ -294,141 +486,3 @@ func ExtractImageURL(htmlContent string) string {
|
|||||||
}
|
}
|
||||||
return htmlContent
|
return htmlContent
|
||||||
}
|
}
|
||||||
|
|
||||||
//func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer) error {
|
|
||||||
// file := excelize.NewFile()
|
|
||||||
// sheet := "Sheet1"
|
|
||||||
//
|
|
||||||
// _, err := file.NewSheet(sheet)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// sort.Slice(questions, func(i, j int) bool {
|
|
||||||
// return questions[i].Page > questions[j].Page
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// headers := []string{"Данные респондента"}
|
|
||||||
// mapQueRes := make(map[uint64]string)
|
|
||||||
//
|
|
||||||
// for _, q := range questions {
|
|
||||||
// if !q.Deleted {
|
|
||||||
// if q.Type == model.TypeResult {
|
|
||||||
// mapQueRes[q.Id] = q.Title + "\n" + q.Description
|
|
||||||
// } else {
|
|
||||||
// headers = append(headers, q.Title)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// headers = append(headers, "Результат")
|
|
||||||
//
|
|
||||||
// // добавляем заголовки в первую строку
|
|
||||||
// for col, header := range headers {
|
|
||||||
// cell := ToAlphaString(col+1) + "1"
|
|
||||||
// if err := file.SetCellValue(sheet, cell, header); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // мапа для хранения обычных ответов респондентов
|
|
||||||
// standart := make(map[string][]model.Answer)
|
|
||||||
//
|
|
||||||
// // мапа для хранения данных респондентов
|
|
||||||
// results := make(map[string]model.Answer)
|
|
||||||
//
|
|
||||||
// // заполняем мапу ответами и данными респондентов
|
|
||||||
// for _, answer := range answers {
|
|
||||||
// if answer.Result {
|
|
||||||
// results[answer.Session] = answer
|
|
||||||
// } else {
|
|
||||||
// standart[answer.Session] = append(standart[answer.Session], answer)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // записываем данные в файл
|
|
||||||
// row := 2
|
|
||||||
// for session, _ := range results {
|
|
||||||
// response := standart[session]
|
|
||||||
// if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// count := 2
|
|
||||||
// for _, q := range questions {
|
|
||||||
// if !q.Deleted && q.Type != model.TypeResult {
|
|
||||||
// sort.Slice(response, func(i, j int) bool {
|
|
||||||
// return response[i].QuestionId < response[j].QuestionId
|
|
||||||
// })
|
|
||||||
// index := binarySearch(response, q.Id)
|
|
||||||
// if index != -1 {
|
|
||||||
// cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
// typeMap := FileSearch(response[index].Content)
|
|
||||||
// noAccept := make(map[string]struct{})
|
|
||||||
// todoMap := make(map[string]string)
|
|
||||||
// for _, tipe := range typeMap {
|
|
||||||
// if tipe != "Text" && q.Type == model.TypeImages || q.Type == model.TypeVarImages {
|
|
||||||
// urle := ExtractImageURL(response[index].Content)
|
|
||||||
// urlData := strings.Split(urle, " ")
|
|
||||||
// for _, k := range urlData {
|
|
||||||
// u, err := url.Parse(k)
|
|
||||||
// if err == nil && u.Scheme != "" && u.Host != "" {
|
|
||||||
// picture, err := downloadImage(k)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
|
|
||||||
// file.SetRowHeight(sheet, row, 150)
|
|
||||||
// if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// noAccept[response[index].Content] = struct{}{}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else if tipe != "Text" && q.Type == model.TypeFile {
|
|
||||||
// urle := ExtractImageURL(response[index].Content)
|
|
||||||
// display, tooltip := urle, urle
|
|
||||||
// if err := file.SetCellValue(sheet, cell, response[index].Content); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{
|
|
||||||
// Display: &display,
|
|
||||||
// Tooltip: &tooltip,
|
|
||||||
// }); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// noAccept[response[index].Content] = struct{}{}
|
|
||||||
// } else {
|
|
||||||
// todoMap[response[index].Content] = cell
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// for cnt, cel := range todoMap {
|
|
||||||
// if _, ok := noAccept[cnt]; !ok {
|
|
||||||
// if err := file.SetCellValue(sheet, cel, cnt); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// } else {
|
|
||||||
// cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
// if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// count++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
|
||||||
// if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// row++
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // cохраняем данные в буфер
|
|
||||||
// if err := file.Write(buffer); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
|
112
workers/tg_worker.go
Normal file
112
workers/tg_worker.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package workers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"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/core/clients/telegram"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
BotID int64
|
||||||
|
Redis *redis.Client
|
||||||
|
Dal *dal.DAL
|
||||||
|
TgClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type TgListenerWorker struct {
|
||||||
|
botID int64
|
||||||
|
redis *redis.Client
|
||||||
|
dal *dal.DAL
|
||||||
|
tgClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTgListenerWC(deps Deps) *TgListenerWorker {
|
||||||
|
return &TgListenerWorker{
|
||||||
|
botID: deps.BotID,
|
||||||
|
redis: deps.Redis,
|
||||||
|
dal: deps.Dal,
|
||||||
|
tgClient: deps.TgClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *TgListenerWorker) Start(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(10 * time.Second) //time.Minute
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
wc.processTasks(ctx)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *TgListenerWorker) processTasks(ctx context.Context) {
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
var keys []string
|
||||||
|
var err error
|
||||||
|
keys, cursor, err = wc.redis.Scan(ctx, cursor, "telegram_task:*", 0).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed scan for telegram tasks:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
func() {
|
||||||
|
taskBytes, err := wc.redis.GetDel(ctx, key).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Println("Failed getdel telegram task:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// todo logging into tg with trashlog
|
||||||
|
var aimErr error
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil || aimErr != nil {
|
||||||
|
fmt.Println("recovering from panic or error setting redis value:", r, aimErr)
|
||||||
|
_ = wc.redis.Set(ctx, key, taskBytes, 0).Err()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var task model.TgRedisTask
|
||||||
|
if err = json.Unmarshal([]byte(taskBytes), &task); err != nil {
|
||||||
|
fmt.Println("Failed unmarshal telegram task:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var inviteLink string
|
||||||
|
var chatID int64
|
||||||
|
inviteLink, chatID, aimErr = wc.tgClient.CreateChannel(task.Name, wc.botID)
|
||||||
|
if aimErr != nil {
|
||||||
|
fmt.Println("Failed create tg channel:", aimErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, aimErr = wc.dal.AccountRepo.PostLeadTarget(ctx, model.LeadTarget{
|
||||||
|
AccountID: task.AccountID,
|
||||||
|
Type: model.LeadTargetTg,
|
||||||
|
QuizID: task.QuizID,
|
||||||
|
Target: strconv.Itoa(int(chatID)),
|
||||||
|
InviteLink: inviteLink,
|
||||||
|
})
|
||||||
|
if aimErr != nil {
|
||||||
|
fmt.Println("Failed create lead target in db:", aimErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user