Merge branch 'leadTarget' into 'dev'

Lead target

See merge request backend/quiz/core!25
This commit is contained in:
Mikhail 2024-09-19 13:04:45 +00:00
commit 8f56724bac
16 changed files with 1402 additions and 232 deletions

2
.gitignore vendored

@ -20,3 +20,5 @@ worker/worker
storer/storer
answerer/answerer
core
/.tdlib/
/unsetrecover.bolt

@ -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)
@ -178,6 +209,9 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
Producer: producer,
ServiceName: options.ServiceName,
ChDAL: chDal,
TelegramClient: tgClient,
RedisClient: redisClient,
S3Prefix: options.S3Prefix,
})
svc.Register(app)

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

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

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

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

@ -9,6 +9,7 @@ import (
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
"time"
"unicode/utf8"
"fmt"
)
type CreateQuizReq struct {
@ -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,10 +1,12 @@
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
@ -14,6 +16,9 @@ type Service struct {
producer *brokers.Producer
serviceName string
chDAL *dal.ClickHouseDAL
telegramClient *telegram.TelegramClient
redisClient *redis.Client
s3Prefix string
}
type Deps struct {
@ -22,6 +27,9 @@ type Deps struct {
Producer *brokers.Producer
ServiceName string
ChDAL *dal.ClickHouseDAL
TelegramClient *telegram.TelegramClient
RedisClient *redis.Client
S3Prefix string
}
func New(deps Deps) *Service {
@ -31,6 +39,9 @@ func New(deps Deps) *Service {
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

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

@ -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,8 +97,10 @@ 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) {
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)
@ -92,6 +111,83 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
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())
}
}
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 {
index := binarySearch(response, q.Id)
@ -152,38 +248,134 @@ func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []mo
}
}
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 {
cell := ToAlphaString(count) + strconv.Itoa(row)
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
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())
}
}
count++
}
}
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
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
}
} else {
todoMap[imgContent] = 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()
descriptionsStr := strings.Join(descriptions, "\n")
linkText := fmt.Sprintf("%s\n Перейти в приложение %s!A%d", descriptionsStr, mediaSheet, mediaRow)
if err := file.Write(buffer); err != nil {
return err
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())
//}
}
} 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
}
}
}
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

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