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
|
||||
answerer/answerer
|
||||
core
|
||||
/.tdlib/
|
||||
/unsetrecover.bolt
|
||||
|
44
app/app.go
44
app/app.go
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/skeris/appInit"
|
||||
"github.com/themakers/hlog"
|
||||
@ -17,11 +18,13 @@ import (
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||
"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/telegram"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/initialize"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/server"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/service"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/tools"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/workers"
|
||||
"penahub.gitlab.yandexcloud.net/external/trashlog/wrappers/zaptrashlog"
|
||||
"time"
|
||||
)
|
||||
@ -69,6 +72,10 @@ type Options struct {
|
||||
TrashLogHost string `env:"TRASH_LOG_HOST" default:"localhost:7113"`
|
||||
ModuleLogger string `env:"MODULE_LOGGER" default:"core-local"`
|
||||
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) {
|
||||
@ -143,6 +150,16 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
|
||||
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{
|
||||
URL: options.HubAdminUrl,
|
||||
ServiceName: options.ServiceName,
|
||||
@ -152,6 +169,20 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
|
||||
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
|
||||
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 подумать над реализацией всего а то пока мне кажется что немного каша получается такой предикт что через некоторое время
|
||||
// сложно будет разобраться что есть где
|
||||
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
|
||||
|
||||
svc := service.New(service.Deps{
|
||||
Dal: pgdal,
|
||||
AuthClient: authClient,
|
||||
Producer: producer,
|
||||
ServiceName: options.ServiceName,
|
||||
ChDAL: chDal,
|
||||
Dal: pgdal,
|
||||
AuthClient: authClient,
|
||||
Producer: producer,
|
||||
ServiceName: options.ServiceName,
|
||||
ChDAL: chDal,
|
||||
TelegramClient: tgClient,
|
||||
RedisClient: redisClient,
|
||||
S3Prefix: options.S3Prefix,
|
||||
})
|
||||
|
||||
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:
|
||||
core:
|
||||
hostname: squiz-core
|
||||
@ -15,5 +16,13 @@ services:
|
||||
PUBLIC_KEY: $PEM_PUB_USERID
|
||||
PRIVATE_KEY: $PEM_PRIV_USERID
|
||||
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:
|
||||
- 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'
|
||||
PUBLIC_KEY: $PEM_PUB_USERID
|
||||
PRIVATE_KEY: $PEM_PRIV_USERID
|
||||
REDIRECT_URL: 'https://quiz.pena.digital'
|
||||
KAFKA_BROKERS: 10.8.0.6:9092
|
||||
KAFKA_TOPIC: "mailnotifier"
|
||||
GRPC_HOST: "0.0.0.0"
|
||||
|
17
go.mod
17
go.mod
@ -1,16 +1,16 @@
|
||||
module penahub.gitlab.yandexcloud.net/backend/quiz/core
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/gofiber/fiber/v2 v2.52.4
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/pioz/faker v1.7.3
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/skeris/appInit v1.0.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
|
||||
@ -20,10 +20,10 @@ require (
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
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/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c
|
||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5
|
||||
penahub.gitlab.yandexcloud.net/backend/tdlib v0.0.0-20240701075856-1731684c936f
|
||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.6-0.20240827173635-78ce9878c387
|
||||
)
|
||||
|
||||
require (
|
||||
@ -34,7 +34,6 @@ require (
|
||||
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
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // 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/msoleps v1.0.3 // 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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.53.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // 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
|
||||
golang.org/x/crypto v0.24.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/yuin/goldmark v1.1.27/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.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
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.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
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-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.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-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-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-20211025201205-69cdffdb9359/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=
|
||||
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-20240630122905-2747a8c00d81 h1:HSEcPZ8PVOrhj6d7/7MjHibxu/+3KXUeFbd/JpZ//bI=
|
||||
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 h1:0t6pQHJvA3jMeBB3FPUmA+hw8rWlksTah8KJEtf2KD8=
|
||||
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/go.mod h1:zswBuTwmEsFHBVRu1nkG3/Fwylk5Vcm8OUm9iWxccSE=
|
||||
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c h1:imtXaIVscs8it6SfAmDxjNxqQSF44GgCTl1N6JT6unA=
|
||||
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240803124813-79e62d2acf3c/go.mod h1:i7M72RIpkSjcQtHID6KKj9RT/EYZ1rxS6tIPKWa/BSY=
|
||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5 h1:amsK0bkSJxBisk334aFo5ZmVPvN1dBT0Sv5j3V5IsT8=
|
||||
penahub.gitlab.yandexcloud.net/external/trashlog v0.1.5/go.mod h1:J8kQNEP4bL7ZNKHxuT4tfe6a3FHyovpAPkyytN4qllc=
|
||||
penahub.gitlab.yandexcloud.net/backend/tdlib v0.0.0-20240701075856-1731684c936f h1:Qli89wgu0T7nG4VECXZOZ40fjE/hVVfxF3hTaSYS008=
|
||||
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.6-0.20240827173635-78ce9878c387 h1:G+GIhkkvUsM9No2rf2D4kvQ2ExTw9KxlA8vsSnC0ywU=
|
||||
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:
|
||||
type: boolean
|
||||
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:
|
||||
/liveness:
|
||||
get:
|
||||
@ -1558,6 +1622,211 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
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:
|
||||
get:
|
||||
description: получение статистики по векторам прохождения респондентами опроса с ветвлением и без, на выход отдается мапа с ключем последний вопрос и массивом точек "точек прохождения пользователем вопросов" грубо говоря массив с векторами как двигался респондент по возможным путям, в этом массиве question id и count прошедших сессий через него
|
||||
@ -1581,3 +1850,94 @@ paths:
|
||||
description: Bad Request
|
||||
'500':
|
||||
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 (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw"
|
||||
"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/core/brokers"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -91,7 +94,6 @@ func (s *Service) createAccount(ctx *fiber.Ctx) error {
|
||||
newAccount := model.Account{
|
||||
UserID: accountID,
|
||||
CreatedAt: time.Now(),
|
||||
Email: email,
|
||||
Deleted: false,
|
||||
Privileges: map[string]model.ShortPrivilege{
|
||||
"quizUnlimTime": {
|
||||
@ -107,6 +109,15 @@ func (s *Service) createAccount(ctx *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
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{
|
||||
CtxUserID: accountID,
|
||||
@ -236,3 +247,145 @@ func (s *Service) ManualDone(ctx *fiber.Ctx) error {
|
||||
|
||||
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,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
questionID, err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
||||
if err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
@ -75,7 +76,7 @@ func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
|
||||
|
||||
hlogger.Emit(models.InfoQuestionCreate{
|
||||
CtxUserID: accountID,
|
||||
CtxIDInt: int64(req.QuizId),
|
||||
CtxIDInt: int64(req.QuizId),
|
||||
CtxQuestionID: int64(questionID),
|
||||
})
|
||||
|
||||
@ -312,7 +313,7 @@ func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
|
||||
|
||||
hlogger.Emit(models.InfoQuestionDelete{
|
||||
CtxUserID: accountID,
|
||||
CtxIDInt: int64(deleted.QuizId),
|
||||
CtxIDInt: int64(deleted.QuizId),
|
||||
CtxQuestionID: int64(deleted.Id),
|
||||
})
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type CreateQuizReq struct {
|
||||
@ -101,7 +102,7 @@ func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
|
||||
|
||||
hlogger.Emit(models.InfoQuizCreated{
|
||||
CtxUserID: accountId,
|
||||
CtxIDInt: int64(quizID),
|
||||
CtxIDInt: int64(quizID),
|
||||
})
|
||||
|
||||
return ctx.Status(fiber.StatusCreated).JSON(record)
|
||||
@ -314,13 +315,13 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
|
||||
if req.Status == model.StatusStart {
|
||||
hlogger.Emit(models.InfoQuizPublish{
|
||||
CtxUserID: accountId,
|
||||
CtxIDInt: int64(quiz.Id),
|
||||
CtxIDInt: int64(quiz.Id),
|
||||
})
|
||||
}
|
||||
if req.Status == model.StatusStop {
|
||||
hlogger.Emit(models.InfoQuizStop{
|
||||
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{
|
||||
CtxUserID: accountId,
|
||||
CtxIDInt: int64(req.Id),
|
||||
CtxIDInt: int64(req.Id),
|
||||
})
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Println("TEMPLERR", err)
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
|
||||
"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/telegram"
|
||||
)
|
||||
|
||||
// Service is an entity for http requests handling
|
||||
type Service struct {
|
||||
dal *dal.DAL
|
||||
authClient *auth.AuthClient
|
||||
producer *brokers.Producer
|
||||
serviceName string
|
||||
chDAL *dal.ClickHouseDAL
|
||||
dal *dal.DAL
|
||||
authClient *auth.AuthClient
|
||||
producer *brokers.Producer
|
||||
serviceName string
|
||||
chDAL *dal.ClickHouseDAL
|
||||
telegramClient *telegram.TelegramClient
|
||||
redisClient *redis.Client
|
||||
s3Prefix string
|
||||
}
|
||||
|
||||
type Deps struct {
|
||||
Dal *dal.DAL
|
||||
AuthClient *auth.AuthClient
|
||||
Producer *brokers.Producer
|
||||
ServiceName string
|
||||
ChDAL *dal.ClickHouseDAL
|
||||
Dal *dal.DAL
|
||||
AuthClient *auth.AuthClient
|
||||
Producer *brokers.Producer
|
||||
ServiceName string
|
||||
ChDAL *dal.ClickHouseDAL
|
||||
TelegramClient *telegram.TelegramClient
|
||||
RedisClient *redis.Client
|
||||
S3Prefix string
|
||||
}
|
||||
|
||||
func New(deps Deps) *Service {
|
||||
return &Service{
|
||||
dal: deps.Dal,
|
||||
authClient: deps.AuthClient,
|
||||
producer: deps.Producer,
|
||||
serviceName: deps.ServiceName,
|
||||
chDAL: deps.ChDAL,
|
||||
dal: deps.Dal,
|
||||
authClient: deps.AuthClient,
|
||||
producer: deps.Producer,
|
||||
serviceName: deps.ServiceName,
|
||||
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.Delete("/account/:userId", s.deleteAccountByUserID)
|
||||
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
|
||||
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", s.AllServiceStatistics)
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/xuri/excelize/v2"
|
||||
_ "image/gif"
|
||||
@ -27,7 +28,7 @@ const (
|
||||
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()
|
||||
sheet := "Sheet1"
|
||||
|
||||
@ -40,6 +41,35 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
||||
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{"Данные респондента"}
|
||||
mapQueRes := make(map[uint64]string)
|
||||
|
||||
@ -52,27 +82,14 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headers = append(headers, "Результат")
|
||||
return headers, mapQueRes
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// мапа для хранения обычных ответов респондентов
|
||||
func categorizeAnswers(answers []model.Answer) (map[string][]model.Answer, map[string]model.Answer) {
|
||||
standart := make(map[string][]model.Answer)
|
||||
|
||||
// мапа для хранения данных респондентов
|
||||
results := make(map[string]model.Answer)
|
||||
|
||||
// заполняем мапу ответами и данными респондентов
|
||||
for _, answer := range answers {
|
||||
if answer.Result {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return standart, results
|
||||
}
|
||||
|
||||
processSession := func(session string, response []model.Answer, row int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Recovered from panic:", r)
|
||||
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() {
|
||||
if r := recover(); r != nil {
|
||||
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())
|
||||
}
|
||||
if flag == -1 {
|
||||
_, _ = file.NewSheet(mediaSheet)
|
||||
err = file.SetCellValue(mediaSheet, "A1", "Вопрос")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
count := 2
|
||||
for _, q := range questions {
|
||||
if !q.Deleted && q.Type != model.TypeResult {
|
||||
@ -152,38 +248,134 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
cell := ToAlphaString(count) + strconv.Itoa(row)
|
||||
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
||||
mediaRow := row
|
||||
for i, imgContent := range multiImgArr {
|
||||
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())
|
||||
}
|
||||
|
||||
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)
|
||||
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
} else {
|
||||
if len(content) > 1 && content[0] == '`' && content[len(content)-1] == '`' {
|
||||
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
|
||||
var wg sync.WaitGroup
|
||||
for session, _ := range results {
|
||||
wg.Add(1)
|
||||
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
|
||||
func handleFile(file *excelize.File, sheet, cell, content, s3Prefix string, noAccept map[string]struct{}) {
|
||||
urle := content
|
||||
if urle != "" && !strings.HasPrefix(urle, "https") {
|
||||
urle = s3Prefix + urle
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -294,141 +486,3 @@ func ExtractImageURL(htmlContent string) string {
|
||||
}
|
||||
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