Merge branch 'dev' into gigachat

This commit is contained in:
skeris 2025-05-15 00:40:59 +03:00
commit 3997a9bb93
79 changed files with 3093 additions and 1437 deletions

@ -15,9 +15,18 @@ jobs:
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
ValidateConfig:
runs-on: [squizprod]
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/validate_config.yml@v1.2.1
needs: CreateImage
with:
runner: hubstaging
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [squizprod]
needs: CreateImage
needs: ValidateConfig
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: hubprod

@ -8,16 +8,35 @@ on:
jobs:
CreateImage:
runs-on: [hubstaging]
runs-on: [squizstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: hubstaging
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [hubstaging]
ValidateConfig:
runs-on: [squizstaging]
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/validate_config.yml@v1.2.1
needs: CreateImage
with:
runner: hubstaging
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
MigrateDatabase:
runs-on: [squizstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/migrate.yml@9263e22095fa40bcb36881ad81722d3049acd07f
needs: ValidateConfig
with:
runner: hubstaging
branch_name: ${{ github.ref_name }}
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [squizstaging]
needs: MigrateDatabase
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: hubstaging

@ -8,7 +8,7 @@ on:
jobs:
Lint:
runs-on: [hubstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.0
runs-on: [squizstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.2
with:
runner: hubstaging
runner: squizstaging

2
.gitignore vendored

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

3
CHANGELOG.md Normal file

@ -0,0 +1,3 @@
#v1.0.0
- В статистику по воронкам добавлена статистика по формам контактов

@ -1,12 +1,13 @@
FROM gitea.pena/penadevops/container-images/golang:main as build
WORKDIR /app
RUN apk add git
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o core
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o core ./cmd/main.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o validator ./cmd/validator/main.go
FROM gitea.pena/penadevops/container-images/alpine:main as prod
FROM gitea.pena/penadevops/container-images/alpine:main
COPY --from=build /app/core .
COPY --from=build /app/schema /schema
COPY --from=build /app/validator .
RUN apk add tzdata
CMD ["/core"]

@ -1,64 +0,0 @@
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
COMMIT?=$(shell git rev-parse --short HEAD)
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
GOOS?=linux
GOARCH?=amd64
DOCKER_REGISTRY=yourRegistryHost:<port>
BINARY_NAME=$(shell basename `pwd`)
PORT?=1488
SHELL = /bin/bash
LDFLAGS=-s -w -X github.com/skeris/appInit/version.Release=${shell git describe --tags --abbrev=0} \
-X github.com/skeris/appInit/version.Commit=${COMMIT} -X github.com/skeris/appInit/version.BuildTime=${BUILD_TIME}
all: compile run
clean:
rm -f $(BINARY_NAME)
rm -f ./worker/worker
compile: clean
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} $(GOBUILD) -ldflags "${LDFLAGS}" -o ${BINARY_NAME}
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} $(GOBUILD) -ldflags "${LDFLAGS}" -o ./worker/worker
run: compile
./$(BINARY_NAME)
container: compile
docker build -t $(BINARY_NAME):${shell git describe --tags --abbrev=0} .
docker build -t $(BINARY_NAME)-worker:${shell git describe --tags --abbrev=0} ./worker
docker-push: container
docker tag $(BINARY_NAME) $(DOCKER_REGISTRY)/$(BINARY_NAME)
docker tag $(BINARY_NAME)-worker $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker
pull:
docker pull $(DOCKER_REGISTRY)/$(BINARY_NAME)
docker pull $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker
docker tag $(DOCKER_REGISTRY)/$(BINARY_NAME) $(BINARY_NAME)
docker tag $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker $(BINARY_NAME)-worker
run-container:
docker run --rm --name squiz --network host -p 1488:1488 $(BINARY_NAME):latest
test:
$(GOTEST) -v -race ./...
commit-all:
git add -A
git commit -a
git push
push-new-release: commit-all
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print "v"$$1+1".0.0"}'}
git push --tags
push-new-feature: commit-all
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print $$1"."$$2+1".0"}'}
git push --tags
push-new-state: commit-all
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print $$1"."$$2"."$$3+1}'}
git push --tags
benchmark:
mv ./tests/new.txt ./tests/old.txt
go test -run=NONE -bench=. -benchmem ./tests -test.short > ./tests/new.txt
benchstat -html ./tests/old.txt ./tests/new.txt > benchmark.html
# show full set of messages
test-in-docker-debug:
docker-compose -f deployments/test/docker-compose.yaml up --build --force-recreate
# show only relevant messages
test-in-docker:
docker-compose -f deployments/test/docker-compose.yaml up --build --force-recreate --exit-code-from test-squiz 2>/dev/null | grep ^test-squiz

8
Taskfile.dist.yml Normal file

@ -0,0 +1,8 @@
tasks:
update-linter:
cmds:
- go get -u gitea.pena/PenaSide/linters-golang
lint:
cmds:
- task: update-linter
- cmd: golangci-lint run -v -c $(go list -f '{{"{{"}}.Dir{{"}}"}}' -m gitea.pena/PenaSide/linters-golang)/.golangci.yml

@ -733,11 +733,23 @@ components:
description: Идентификатор вопроса
PipeLineStatsResp:
type: object
properties:
PipelineStatistic:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/Statistic'
description: Статистика по воронкам
ContactFormStatistic:
type: object
additionalProperties:
type: integer
format: int64
description: Количество ответов на вопрос формы контактов
description: Статистика форм контакта
Answer:
type: object
properties:
@ -772,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:
@ -1546,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 прошедших сессий через него
@ -1569,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: внутренняя ошибка сервера

@ -1,239 +0,0 @@
package app
import (
"context"
"errors"
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/PenaSide/common/privilege"
"gitea.pena/PenaSide/hlog"
"gitea.pena/PenaSide/trashlog/wrappers/zaptrashlog"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/healthchecks"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/core/brokers"
"gitea.pena/SQuiz/core/clients/auth"
"gitea.pena/SQuiz/core/initialize"
"gitea.pena/SQuiz/core/models"
"gitea.pena/SQuiz/core/server"
"gitea.pena/SQuiz/core/service"
"gitea.pena/SQuiz/core/tools"
"github.com/gofiber/fiber/v2"
"github.com/skeris/appInit"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
)
type App struct {
logger *zap.Logger
err chan error
}
func (a App) GetLogger() *zap.Logger {
return a.logger
}
func (a App) GetErr() chan error {
return a.err
}
var (
errInvalidOptions = errors.New("invalid options")
)
var zapOptions = []zap.Option{
zap.AddCaller(),
zap.AddCallerSkip(2),
zap.AddStacktrace(zap.ErrorLevel),
}
var _ appInit.CommonApp = (*App)(nil)
type Options struct {
LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
IsProd bool `env:"IS_PROD" default:"false"`
NumberPort string `env:"PORT" default:"1488"`
CrtFile string `env:"CRT" default:"server.crt"`
KeyFile string `env:"KEY" default:"server.key"`
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
HubAdminUrl string `env:"HUB_ADMIN_URL" default:"http://localhost:8001/"`
ServiceName string `env:"SERVICE_NAME" default:"squiz"`
AuthServiceURL string `env:"AUTH_URL" default:"http://localhost:8000/"`
GrpcHost string `env:"GRPC_HOST" default:"localhost"`
GrpcPort string `env:"GRPC_PORT" default:"9000"`
KafkaBrokers string `env:"KAFKA_BROKERS" default:"localhost:9092"`
KafkaTopic string `env:"KAFKA_TOPIC" default:"test-topic"`
KafkaGroup string `env:"KAFKA_GROUP" default:"mailnotifier"`
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"`
S3Prefix string `env:"S3_PREFIX"`
KafkaGroupGigaChat string `env:"KAFKA_GROUP_GIGA_CHAT" default:"gigachat"`
KafkaTopicGigaChat string `env:"KAFKA_TOPIC_GIGA_CHAT"`
}
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
var (
err, workerErr error
zapLogger *zap.Logger
errChan = make(chan error)
options Options
ok bool
)
if options, ok = opts.(Options); !ok {
return App{}, errInvalidOptions
}
if options.LoggerProdMode {
zapLogger, err = zap.NewProduction(zapOptions...)
if err != nil {
return nil, err
}
} else {
zapLogger, err = zap.NewDevelopment(zapOptions...)
if err != nil {
return nil, err
}
}
zapLogger = zapLogger.With(
zap.String("SvcCommit", ver.Commit),
zap.String("SvcVersion", ver.Release),
zap.String("SvcBuildTime", ver.BuildTime),
)
clickHouseLogger, err := zaptrashlog.NewCore(ctx, zap.InfoLevel, options.TrashLogHost, ver.Release, ver.Commit, time.Now().Unix())
if err != nil {
panic(err)
}
loggerForHlog := zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewTee(core, clickHouseLogger)
}))
loggerHlog := hlog.New(loggerForHlog).Module(options.ModuleLogger)
loggerHlog.With(models.AllFields{})
loggerHlog.Emit(InfoSvcStarted{})
authClient := auth.NewAuthClient(options.AuthServiceURL)
pgdal, err := dal.New(ctx, options.PostgresCredentials, nil)
if err != nil {
fmt.Println("NEW", err)
return nil, err
}
chDal, err := dal.NewClickHouseDAL(ctx, options.ClickHouseCred)
if err != nil {
fmt.Println("failed init clickhouse", err)
return nil, err
}
kafkaClient, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
KafkaGroup: options.KafkaGroup,
KafkaBrokers: options.KafkaBrokers,
KafkaTopic: options.KafkaTopic,
})
if err != nil {
return nil, err
}
kafkaClientGigaChat, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
KafkaGroup: options.KafkaGroupGigaChat,
KafkaBrokers: options.KafkaBrokers,
KafkaTopic: options.KafkaTopicGigaChat,
})
producer := brokers.NewProducer(brokers.ProducerDeps{
KafkaClient: kafkaClient,
Logger: zapLogger,
})
producerGigaChat := brokers.NewProducer(brokers.ProducerDeps{
KafkaClient: kafkaClientGigaChat,
Logger: zapLogger,
})
clientData := privilege.Client{
URL: options.HubAdminUrl,
ServiceName: options.ServiceName,
Privileges: model.Privileges,
}
fiberClient := &fiber.Client{}
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
go tools.PublishPrivilege(privilegeController, 10, 5*time.Minute)
// todo подумать над реализацией всего а то пока мне кажется что немного каша получается такой предикт что через некоторое время
// сложно будет разобраться что есть где
grpcControllers := initialize.InitRpcControllers(pgdal)
grpc, err := server.NewGRPC(zapLogger)
if err != nil {
fmt.Println("error:", err)
panic("err init grpc server")
}
grpc.Register(grpcControllers)
go grpc.Run(server.DepsGrpcRun{
Host: options.GrpcHost,
Port: options.GrpcPort,
})
app := fiber.New()
app.Use(middleware.JWTAuth())
app.Use(log_mw.ContextLogger(loggerHlog))
app.Get("/liveness", healthchecks.Liveness)
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,
S3Prefix: options.S3Prefix,
ProducerGigaChat: producerGigaChat,
})
svc.Register(app)
loggerHlog.Emit(InfoSvcReady{})
go func() {
defer func() {
if pgdal != nil {
pgdal.Close()
}
if chDal != nil {
chDal.Close(ctx)
}
err := grpc.Stop(ctx)
err = app.Shutdown()
loggerHlog.Emit(InfoSvcShutdown{Signal: err.Error()})
}()
if options.IsProd {
if err := app.ListenTLS(fmt.Sprintf(":%s", options.NumberPort), options.CrtFile, options.KeyFile); err != nil {
loggerHlog.Emit(ErrorCanNotServe{
Err: err,
})
errChan <- err
}
} else {
if err := app.Listen(fmt.Sprintf(":%s", options.NumberPort)); err != nil {
loggerHlog.Emit(ErrorCanNotServe{
Err: err,
})
errChan <- err
}
}
errChan <- nil
}()
// todo implement helper func for service app type. such as server preparing, logger preparing, healthchecks and etc.
return &App{
logger: zapLogger,
err: errChan,
}, err
}

@ -0,0 +1,329 @@
package benchmarks
import (
"database/sql"
"log"
"testing"
_ "github.com/lib/pq"
)
const (
accountID = "64f2cd7a7047f28fdabf6d9e"
connStr = "host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"
queryTotal = `
WITH user_data AS (
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
)
SELECT f.*, COUNT(*) OVER() as total_count
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
WHERE f.Deleted = false
ORDER BY f.ID OFFSET ($2 - 1) * $3 LIMIT $3;
`
queryCount = `
WITH user_data AS (
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
)
SELECT COUNT(*)
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
WHERE f.Deleted = false;
`
queryData = `
WITH user_data AS (
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
)
SELECT f.*
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
WHERE f.Deleted = false
ORDER BY f.ID OFFSET ($2 - 1) * $3 LIMIT $3;
`
)
type GetFieldsWithPaginationRow struct {
ID int64 `db:"id" json:"id"`
Amoid int32 `db:"amoid" json:"amoid"`
Code string `db:"code" json:"code"`
Accountid int32 `db:"accountid" json:"accountid"`
Name string `db:"name" json:"name"`
Entity interface{} `db:"entity" json:"entity"`
Type interface{} `db:"type" json:"type"`
Deleted bool `db:"deleted" json:"deleted"`
Createdat sql.NullTime `db:"createdat" json:"createdat"`
TotalCount int64 `db:"total_count" json:"total_count"`
}
func initDB() *sql.DB {
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
return db
}
// Все получаем в одном запросе не аллоцируя при этом массив
func BenchmarkAllOne(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 25
rows, err := db.Query(queryTotal, accountID, page, size)
if err != nil {
b.Fatal(err)
}
defer rows.Close()
var results []GetFieldsWithPaginationRow
for rows.Next() {
var row GetFieldsWithPaginationRow
if err := rows.Scan(
&row.ID,
&row.Amoid,
&row.Code,
&row.Accountid,
&row.Name,
&row.Entity,
&row.Type,
&row.Deleted,
&row.Createdat,
&row.TotalCount,
); err != nil {
b.Fatal(err)
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
b.Fatal(err)
}
}
}
// Все получаем в одном запросе аллоцируя при этом массив
func BenchmarkAllOnePreAllocation(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 25
rows, err := db.Query(queryTotal, accountID, page, size)
if err != nil {
b.Fatal(err)
}
defer rows.Close()
results := make([]GetFieldsWithPaginationRow, size)
for rows.Next() {
var row GetFieldsWithPaginationRow
if err := rows.Scan(
&row.ID,
&row.Amoid,
&row.Code,
&row.Accountid,
&row.Name,
&row.Entity,
&row.Type,
&row.Deleted,
&row.Createdat,
&row.TotalCount,
); err != nil {
b.Fatal(err)
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
b.Fatal(err)
}
}
}
// Считается сначала количество потом получаются данные длину и емкость массиву не меняем
func BenchmarkCountThenGetData(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 25
row := db.QueryRow(queryCount, accountID)
var totalCount int
if err := row.Scan(&totalCount); err != nil {
b.Fatal(err)
}
var results []GetFieldsWithPaginationRow
rows, err := db.Query(queryData, accountID, page, size)
if err != nil {
b.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var row GetFieldsWithPaginationRow
if err := rows.Scan(
&row.ID,
&row.Amoid,
&row.Code,
&row.Accountid,
&row.Name,
&row.Entity,
&row.Type,
&row.Deleted,
&row.Createdat,
); err != nil {
b.Fatal(err)
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
b.Fatal(err)
}
}
}
// Параллельное вычисление данных и общего количество при этом длина слайса = size
func BenchmarkParallel(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 25
results := make([]GetFieldsWithPaginationRow, size)
channel := make(chan error, 2)
go func() {
row := db.QueryRow(queryCount, accountID)
var totalCount int
channel <- row.Scan(&totalCount)
}()
go func() {
rows, err := db.Query(queryData, accountID, page, size)
if err != nil {
channel <- err
return
}
defer rows.Close()
index := 0
for rows.Next() {
if err := rows.Scan(
&results[index].ID,
&results[index].Amoid,
&results[index].Code,
&results[index].Accountid,
&results[index].Name,
&results[index].Entity,
&results[index].Type,
&results[index].Deleted,
&results[index].Createdat,
); err != nil {
channel <- err
return
}
index++
}
channel <- rows.Err()
}()
for i := 0; i < 2; i++ {
if err := <-channel; err != nil {
b.Fatal(err)
}
}
}
}
// Считается сначала количество потом получаются данные создаем слайс через маке указывая ему длину начальную кап = лен
func BenchmarkWithPreAllocation(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 25
results := make([]GetFieldsWithPaginationRow, size)
row := db.QueryRow(queryCount, accountID)
var totalCount int
if err := row.Scan(&totalCount); err != nil {
b.Fatal(err)
}
rows, err := db.Query(queryData, accountID, page, size)
if err != nil {
b.Fatal(err)
}
defer rows.Close()
index := 0
for rows.Next() {
if err := rows.Scan(
&results[index].ID,
&results[index].Amoid,
&results[index].Code,
&results[index].Accountid,
&results[index].Name,
&results[index].Entity,
&results[index].Type,
&results[index].Deleted,
&results[index].Createdat,
); err != nil {
b.Fatal(err)
}
index++
}
if err := rows.Err(); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWithPreAllocationAndMonitoringTotalCount(b *testing.B) {
db := initDB()
defer db.Close()
for i := 0; i < b.N; i++ {
page := 1
size := 50
row := db.QueryRow(queryCount, accountID)
var totalCount int
if err := row.Scan(&totalCount); err != nil {
b.Fatal(err)
}
if totalCount < size {
size = totalCount
}
results := make([]GetFieldsWithPaginationRow, size)
rows, err := db.Query(queryData, accountID, page, size)
if err != nil {
b.Fatal(err)
}
defer rows.Close()
index := 0
for rows.Next() {
if err := rows.Scan(
&results[index].ID,
&results[index].Amoid,
&results[index].Code,
&results[index].Accountid,
&results[index].Name,
&results[index].Entity,
&results[index].Type,
&results[index].Deleted,
&results[index].Createdat,
); err != nil {
b.Fatal(err)
}
index++
}
if err := rows.Err(); err != nil {
b.Fatal(err)
}
}
}

35
cmd/main.go Normal file

@ -0,0 +1,35 @@
package main
import (
"context"
"go.uber.org/zap"
"log"
"os"
"os/signal"
"gitea.pena/SQuiz/core/internal/app"
"gitea.pena/SQuiz/core/internal/initialize"
"syscall"
)
var (
commit string = os.Getenv("COMMIT")
buildTime string = os.Getenv("BUILD_TIME")
version string = os.Getenv("VERSION")
)
func main() {
config, err := initialize.LoadConfig()
if err != nil {
log.Fatal("Failed to load config", zap.Error(err))
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err = app.Run(ctx, *config, app.Build{
Commit: commit,
Version: version,
}); err != nil {
log.Fatal("App exited with error", zap.Error(err))
}
}

85
cmd/validator/main.go Normal file

@ -0,0 +1,85 @@
package main
import (
"context"
"errors"
"gitea.pena/PenaSide/common/validate"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/core/internal/initialize"
"github.com/caarlos0/env/v8"
"log"
)
func main() {
cfg, err := loadConfig()
if err != nil {
log.Fatalf("error loading config: %v", err)
}
err = validateNotEmpty(cfg)
if err != nil {
log.Fatalf("error validating config: %v", err)
}
_, err = dal.New(context.TODO(), cfg.PostgresURL, nil)
if err != nil {
log.Fatalf("error connecting to database postgres: %v", err)
}
_, err = dal.NewClickHouseDAL(context.TODO(), cfg.ClickhouseURL)
if err != nil {
log.Fatalf("error connecting to database clickhouse: %v", err)
}
err = validate.ValidateKafka([]string{cfg.KafkaBrokers}, cfg.KafkaTopicNotifyer)
if err != nil {
log.Fatalf("error validating kafka: %v", err)
}
err = validate.ValidateRedis(cfg.RedisHost, cfg.RedisPassword, int(cfg.RedisDB))
if err != nil {
log.Fatalf("error validating redis: %v", err)
}
return
}
func loadConfig() (initialize.Config, error) {
var cfg initialize.Config
if err := env.Parse(&cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func validateNotEmpty(cfg initialize.Config) error {
if cfg.ClientHttpURL == "" {
return errors.New("client http url dont be empty")
}
if cfg.GrpcURL == "" {
return errors.New("grpc url dont be empty")
}
if cfg.HubadminMicroserviceURL == "" {
return errors.New("hubadmin microservice url dont be empty")
}
if cfg.AuthMicroserviceURL == "" {
return errors.New("auth microservice url dont be empty")
}
if cfg.TrashLogHost == "" {
return errors.New("trash log host dont be empty")
}
if cfg.S3Prefix == "" {
return errors.New("s3 prefix dont be empty")
}
if cfg.ServiceName == "" {
return errors.New("service name dont be empty")
}
return nil
}

@ -0,0 +1,22 @@
IS_PROD_LOG="false"
IS_PROD="false"
CLIENT_HTTP_URL="0.0.0.0:1488"
GRPC_URL="0.0.0.0:9000"
POSTGRES_URL="host=10.7.0.10 port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"
CLICKHOUSE_URL="clickhouse://10.7.0.5:9000/default?sslmode=disable"
HUBADMIN_MICROSERVICE_URL="http://10.7.0.6:59303"
AUTH_MICROSERVICE_URL="http://10.7.0.6:59300/user"
KAFKA_BROKERS="10.7.0.6:9092"
KAFKA_TOPIC="mailnotifier"
KAFKA_GROUP="mailnotifier"
TRASH_LOG_HOST="10.7.0.5:7113"
S3_PREFIX="3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b"
REDIS_HOST="10.7.0.6:6379"
REDIS_PASSWORD="Redalert2"
REDIS_DB=2
PUBLIC_ACCESS_SECRET_KEY="-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE
r7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM
ZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w
KKFD0I2Dr59BFVSGJwIDAQAB
-----END PUBLIC KEY-----"

@ -1,27 +1,14 @@
version: "3"
services:
core:
hostname: squiz-core
container_name: squiz-core
image: $CI_REGISTRY_IMAGE/staging-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: squiz
container_name: squiz
tty: true
environment:
HUB_ADMIN_URL: 'http://10.8.0.6:59303'
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PORT: 1488
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PG_CRED: 'host=10.8.0.5 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
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"
TRASH_LOG_HOST: "10.8.0.15:7113"
MODULE_LOGGER: "quiz-core-staging"
CLICK_HOUSE_CRED: "clickhouse://10.8.0.15:9000/default?sslmode=disable"
image: gitea.pena/squiz/core/staging:$GITHUB_RUN_NUMBER
labels:
com.pena.allowed_headers: content-type,authorization,device,browser,os,devicetype,response-type
env_file: config.env
ports:
- 10.8.0.5:1488:1488
- 10.8.0.5:9000:9000
- 10.7.0.10:1488:1488
- 10.7.0.10:9000:9000
- 10.7.0.10:2346:2345
command: dlv --listen=:2345 --continue --headless=true --log=true --log-output=debugger,debuglineerr,gdbwire,lldbout,rpc --accept-multiclient --api-version=2 exec /core

@ -0,0 +1,6 @@
services:
validator:
tty: true
command: ./validator
image: gitea.pena/squiz/core/staging:$GITHUB_RUN_NUMBER
env_file: config.env

1
go.mod

@ -38,6 +38,7 @@ require (
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect

23
go.sum

@ -35,7 +35,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06 h1:6aQNgrBLzcUBaJHQjMk4X+jDo9rQtu5E0XNLhRV6pOk=
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
@ -59,6 +58,10 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@ -84,7 +87,10 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@ -139,6 +145,8 @@ github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5
github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@ -251,8 +259,8 @@ golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -260,6 +268,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -288,9 +297,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@ -314,6 +326,7 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -1,16 +0,0 @@
package initialize
import (
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/core/rpc_service"
)
type RpcRegister struct {
MailNotify *rpc_service.MailNotify
}
func InitRpcControllers(dal *dal.DAL) *RpcRegister {
return &RpcRegister{
MailNotify: rpc_service.NewMailNotify(dal),
}
}

185
internal/app/app.go Normal file

@ -0,0 +1,185 @@
package app
import (
"context"
"errors"
"gitea.pena/PenaSide/common/privilege"
"gitea.pena/PenaSide/hlog"
"gitea.pena/PenaSide/trashlog/wrappers/zaptrashlog"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/core/internal/brokers"
"gitea.pena/SQuiz/core/internal/initialize"
"gitea.pena/SQuiz/core/internal/models"
server "gitea.pena/SQuiz/core/internal/server/grpc"
"gitea.pena/SQuiz/core/internal/server/http"
"gitea.pena/SQuiz/core/internal/tools"
"gitea.pena/SQuiz/core/internal/workers"
"gitea.pena/SQuiz/core/pkg/closer"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
)
type Build struct {
Commit string
Version string
}
var zapOptions = []zap.Option{
zap.AddCaller(),
zap.AddCallerSkip(2),
zap.AddStacktrace(zap.ErrorLevel),
}
func Run(ctx context.Context, cfg initialize.Config, build Build) error {
var (
err error
zapLogger *zap.Logger
)
defer func() {
if r := recover(); r != nil {
log.Error("Recovered from a panic", zap.Any("error", r))
}
}()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cfg.LoggerProdMode {
zapLogger, err = zap.NewProduction(zapOptions...)
if err != nil {
return err
}
} else {
zapLogger, err = zap.NewDevelopment(zapOptions...)
if err != nil {
return err
}
}
zapLogger = zapLogger.With(
zap.String("SvcCommit", build.Commit),
zap.String("SvcVersion", build.Version),
zap.String("SvcBuildTime", time.Now().String()),
)
clickHouseLogger, err := zaptrashlog.NewCore(ctx, zap.InfoLevel, cfg.TrashLogHost, build.Version, build.Commit, time.Now().Unix())
if err != nil {
panic(err)
}
loggerForHlog := zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewTee(core, clickHouseLogger)
}))
loggerHlog := hlog.New(loggerForHlog).Module(initialize.ModuleLogger)
loggerHlog.With(models.AllFields{})
loggerHlog.Emit(InfoSvcStarted{})
shutdownGroup := closer.NewCloserGroup()
dalS, err := initialize.NewDALs(ctx, cfg)
if err != nil {
zapLogger.Error("Error initializing dals", zap.Error(err))
return err
}
kafkaClient, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
KafkaGroup: cfg.KafkaGroup,
KafkaBrokers: cfg.KafkaBrokers,
KafkaTopic: cfg.KafkaTopicNotifyer,
})
if err != nil {
zapLogger.Error("Error initializing kafka", zap.Error(err))
return err
}
producer := brokers.NewProducer(brokers.ProducerDeps{
KafkaClient: kafkaClient,
Logger: zapLogger,
})
redisClient, err := initialize.Redis(ctx, cfg)
if err != nil {
zapLogger.Error("Error initializing redis", zap.Error(err))
return err
}
go tools.PublishPrivilege(privilege.NewPrivilege(privilege.Client{
URL: cfg.HubadminMicroserviceURL,
ServiceName: cfg.ServiceName,
Privileges: model.Privileges,
}, &fiber.Client{}), 10, 5*time.Minute)
clients, err := initialize.NewClients(ctx, cfg, dalS.PgDAL)
if err != nil {
zapLogger.Error("Error initializing clients", zap.Error(err))
return err
}
tgWC := workers.NewTgListenerWC(workers.Deps{
BotID: int64(6712573453), // todo убрать
Redis: redisClient,
Dal: dalS.PgDAL,
//TgClient: clients.TgClient,
})
go tgWC.Start(ctx)
controllers := initialize.NewControllers(initialize.ControllerDeps{
Clients: clients,
DALs: dalS,
Config: cfg,
Producer: producer,
RedisClient: redisClient,
})
grpc, err := server.NewGRPC(zapLogger)
if err != nil {
zapLogger.Error("Error initializing grpc", zap.Error(err))
return err
}
grpc.Register(controllers.GRpcControllers)
srv := http.NewServer(http.ServerConfig{
Logger: zapLogger,
Controllers: []http.Controller{controllers.HttpControllers.Account, controllers.HttpControllers.Telegram, controllers.HttpControllers.Result,
controllers.HttpControllers.Question, controllers.HttpControllers.Quiz, controllers.HttpControllers.Statistic},
Hlogger: loggerHlog,
})
go func() {
if err := srv.Start(cfg.ClientHttpURL); err != nil {
zapLogger.Error("HTTP server startup error", zap.Error(err))
cancel()
}
}()
go grpc.Run(cfg.GrpcURL)
srv.ListRoutes()
shutdownGroup.Add(closer.CloserFunc(srv.Shutdown))
shutdownGroup.Add(closer.CloserFunc(grpc.Stop))
shutdownGroup.Add(closer.CloserFunc(dalS.PgDAL.Close))
shutdownGroup.Add(closer.CloserFunc(dalS.ChDAL.Close))
<-ctx.Done()
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer timeoutCancel()
if err := shutdownGroup.Call(timeoutCtx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
zapLogger.Error("Shutdown timed out", zap.Error(err))
} else {
zapLogger.Error("Failed to shutdown services gracefully", zap.Error(err))
}
return err
}
zapLogger.Info("Application has stopped")
return nil
}

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

@ -0,0 +1,420 @@
package account
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/pj_errors"
"gitea.pena/SQuiz/core/internal/brokers"
"gitea.pena/SQuiz/core/internal/clients/auth"
"gitea.pena/SQuiz/core/internal/models"
"github.com/go-redis/redis/v8"
"github.com/gofiber/fiber/v2"
"strconv"
"time"
)
type Deps struct {
Dal *dal.DAL
AuthClient *auth.AuthClient
Producer *brokers.Producer
ServiceName string
RedisClient *redis.Client
}
type Account struct {
dal *dal.DAL
authClient *auth.AuthClient
producer *brokers.Producer
serviceName string
redisClient *redis.Client
}
func NewAccountController(deps Deps) *Account {
return &Account{
dal: deps.Dal,
authClient: deps.AuthClient,
producer: deps.Producer,
serviceName: deps.ServiceName,
redisClient: deps.RedisClient,
}
}
type CreateAccountReq struct {
UserID string `json:"userId"`
}
type CreateAccountResp struct {
CreatedAccount model.Account `json:"created_account"`
}
type DeleteAccountResp struct {
DeletedAccountID string `json:"account_Id"`
}
type GetPrivilegeByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDResp struct {
DeletedAccountUserID string `json:"userId"`
}
type GetAccountsReq struct {
Limit uint64 `json:"limit"`
Page uint64 `json:"page"`
}
type GetAccountsResp struct {
Count uint64 `json:"count"`
Items []model.Account `json:"items"`
}
// getCurrentAccount обработчик для получения текущего аккаунта
func (r *Account) GetCurrentAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil && err != sql.ErrNoRows {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
//TODO: fix this later
if account.ID == "" {
return ctx.Status(fiber.StatusNotFound).SendString("no account")
}
return ctx.Status(fiber.StatusOK).JSON(account)
}
// createAccount обработчик для создания нового аккаунта
func (r *Account) CreateAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
hlogger := log_mw.ExtractLogger(ctx)
existingAccount, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil && err != sql.ErrNoRows {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID != "" {
return ctx.Status(fiber.StatusConflict).SendString("user with this ID already exists")
}
email, err := r.authClient.GetUserEmail(accountID)
if err != nil {
return err
}
newAccount := model.Account{
UserID: accountID,
CreatedAt: time.Now(),
Deleted: false,
Privileges: map[string]model.ShortPrivilege{
"quizUnlimTime": {
PrivilegeID: "quizUnlimTime",
PrivilegeName: "Безлимит Опросов",
Amount: 14,
CreatedAt: time.Now(),
},
},
}
createdAcc, err := r.dal.AccountRepo.CreateAccount(ctx.Context(), &newAccount)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
_, err = r.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,
CtxAccountID: createdAcc.ID,
})
err = r.producer.ToMailNotify(ctx.Context(), brokers.Message{
AccountID: accountID,
Email: email,
ServiceKey: r.serviceName,
SendAt: time.Now(),
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(CreateAccountResp{
CreatedAccount: newAccount,
})
}
// deleteAccount обработчик для удаления текущего аккаунта
func (r *Account) DeleteAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if err := r.dal.AccountRepo.DeleteAccount(ctx.Context(), account.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountResp{
DeletedAccountID: accountID,
})
}
// getPrivilegeByUserID обработчик для получения привилегий аккаунта по ID пользователя
func (r *Account) GetPrivilegeByUserID(ctx *fiber.Ctx) error {
var req GetPrivilegeByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
privilege, err := r.dal.AccountRepo.GetPrivilegesByAccountID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(privilege)
}
// deleteAccountByUserID обработчик для удаления аккаунта по ID пользователя
func (r *Account) DeleteAccountByUserID(ctx *fiber.Ctx) error {
var req DeleteAccountByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
existingAccount, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID == "" {
return ctx.Status(fiber.StatusInternalServerError).SendString("user with this ID not found")
}
if err := r.dal.AccountRepo.DeleteAccount(ctx.Context(), existingAccount.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountByUserIDResp{
DeletedAccountUserID: req.UserID,
})
}
// getAccounts обработчик для получения списка аккаунтов с пагинацией
func (r *Account) GetAccounts(ctx *fiber.Ctx) error {
var req GetAccountsReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
_, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
accounts, totalCount, err := r.dal.AccountRepo.GetAccounts(ctx.Context(), req.Limit, req.Page)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
response := GetAccountsResp{
Count: totalCount,
Items: accounts,
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (r *Account) ManualDone(ctx *fiber.Ctx) error {
var req struct {
Id string `json:"id"`
}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("User id is required")
}
err := r.dal.AccountRepo.ManualDone(ctx.Context(), req.Id)
if err != nil {
if errors.Is(err, pj_errors.ErrNotFound) {
return ctx.Status(fiber.StatusNotFound).SendString("user don't have this privilege")
}
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}
func (r *Account) 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 := r.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 := r.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 := r.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 (r *Account) 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 = r.dal.AccountRepo.DeleteLeadTarget(ctx.Context(), leadID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.SendStatus(fiber.StatusOK)
}
func (r *Account) 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 := r.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 (r *Account) 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 := r.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)
}

@ -0,0 +1,21 @@
package account
import "github.com/gofiber/fiber/v2"
func (r *Account) Register(router fiber.Router) {
router.Get("/account/get", r.GetCurrentAccount)
router.Post("/account/create", r.CreateAccount)
router.Delete("/account/delete", r.DeleteAccount)
router.Get("/accounts", r.GetAccounts)
router.Get("/privilege/:userId", r.GetPrivilegeByUserID)
router.Delete("/account/:userId", r.DeleteAccountByUserID)
router.Post("/account/manualdone", r.ManualDone)
router.Post("/account/leadtarget", r.PostLeadTarget)
router.Delete("/account/leadtarget/:id", r.DeleteLeadTarget)
router.Get("/account/leadtarget/:quizID", r.GetLeadTarget)
router.Put("/account/leadtarget", r.UpdateLeadTarget)
}
func (r *Account) Name() string {
return ""
}

@ -1,15 +1,28 @@
package service
package question
import (
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/core/models"
"gitea.pena/SQuiz/core/internal/models"
"github.com/gofiber/fiber/v2"
"github.com/lib/pq"
"unicode/utf8"
)
type Deps struct {
DAL *dal.DAL
}
type Question struct {
dal *dal.DAL
}
func NewQuestionController(deps Deps) *Question {
return &Question{dal: deps.DAL}
}
// QuestionCreateReq request structure for creating Question
type QuestionCreateReq struct {
QuizId uint64 `json:"quiz_id"` // relation to quiz
@ -23,7 +36,7 @@ type QuestionCreateReq struct {
}
// CreateQuestion service handler for creating question for quiz
func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
func (r *Question) CreateQuestion(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
@ -63,7 +76,8 @@ func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
Page: req.Page,
Content: req.Content,
}
questionID, err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
questionID, err := r.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
if err != nil {
if e, ok := err.(*pq.Error); ok {
if e.Constraint == "quiz_relation" {
@ -103,7 +117,7 @@ type GetQuestionListResp struct {
}
// GetQuestionList handler for paginated list question
func (s *Service) GetQuestionList(ctx *fiber.Ctx) error {
func (r *Question) GetQuestionList(ctx *fiber.Ctx) error {
var req GetQuestionListReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -126,7 +140,7 @@ func (s *Service) GetQuestionList(ctx *fiber.Ctx) error {
"'test','none','file', 'button','select','checkbox'")
}
res, cnt, err := s.dal.QuestionRepo.GetQuestionList(ctx.Context(),
res, cnt, err := r.dal.QuestionRepo.GetQuestionList(ctx.Context(),
req.Limit,
req.Page*req.Limit,
uint64(req.From),
@ -165,7 +179,7 @@ type UpdateResp struct {
}
// UpdateQuestion handler for update question
func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
func (r *Question) UpdateQuestion(ctx *fiber.Ctx) error {
var req UpdateQuestionReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -195,7 +209,7 @@ func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none or empty string")
}
question, err := s.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
question, err := r.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -227,7 +241,7 @@ func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
question.Content = req.Content
}
if err := s.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
if err := r.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -243,7 +257,7 @@ type CopyQuestionReq struct {
}
// CopyQuestion handler for copy question
func (s *Service) CopyQuestion(ctx *fiber.Ctx) error {
func (r *Question) CopyQuestion(ctx *fiber.Ctx) error {
var req CopyQuestionReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -253,7 +267,7 @@ func (s *Service) CopyQuestion(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
question, err := s.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
question, err := r.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -271,8 +285,8 @@ type GetQuestionHistoryReq struct {
}
// GetQuestionHistory handler for history of quiz
func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
var req GetQuizHistoryReq
func (r *Question) GetQuestionHistory(ctx *fiber.Ctx) error {
var req GetQuestionHistoryReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
@ -281,7 +295,7 @@ func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
history, err := s.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
history, err := r.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -289,15 +303,22 @@ func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(history)
}
type DeactivateResp struct {
Deactivated uint64 `json:"deactivated"`
}
// DeleteQuestion handler for fake delete question
func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
func (r *Question) DeleteQuestion(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
hlogger := log_mw.ExtractLogger(ctx)
var req DeactivateReq
var req struct {
Id uint64 `json:"id"`
}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
@ -306,7 +327,7 @@ func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting question is required")
}
deleted, err := s.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
deleted, err := r.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

@ -0,0 +1,16 @@
package question
import "github.com/gofiber/fiber/v2"
func (r *Question) Register(router fiber.Router) {
router.Post("/create", r.CreateQuestion)
router.Post("/getList", r.GetQuestionList)
router.Patch("/edit", r.UpdateQuestion)
router.Post("/copy", r.CopyQuestion)
router.Post("/history", r.GetQuestionHistory)
router.Delete("/delete", r.DeleteQuestion)
}
func (r *Question) Name() string {
return "question"
}

@ -1,19 +1,31 @@
package service
package quiz
import (
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/repository/quiz"
"gitea.pena/SQuiz/core/brokers"
"gitea.pena/SQuiz/core/models"
"gitea.pena/SQuiz/core/internal/models"
"github.com/gofiber/fiber/v2"
"strconv"
"time"
"unicode/utf8"
)
type Deps struct {
DAL *dal.DAL
}
type Quiz struct {
dal *dal.DAL
}
func NewQuizController(deps Deps) *Quiz {
return &Quiz{dal: deps.DAL}
}
type CreateQuizReq struct {
Fingerprinting bool `json:"fingerprinting"` // true if you need to store device id
Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting
@ -38,7 +50,7 @@ type CreateQuizReq struct {
}
// CreateQuiz handler for quiz creating request
func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
func (r *Quiz) CreateQuiz(ctx *fiber.Ctx) error {
var req CreateQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -98,7 +110,7 @@ func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
GroupId: req.GroupId,
}
quizID, err := s.dal.QuizRepo.CreateQuiz(ctx.Context(), &record)
quizID, err := r.dal.QuizRepo.CreateQuiz(ctx.Context(), &record)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -132,7 +144,7 @@ type GetQuizListResp struct {
}
// GetQuizList handler for paginated list quiz
func (s *Service) GetQuizList(ctx *fiber.Ctx) error {
func (r *Quiz) GetQuizList(ctx *fiber.Ctx) error {
var req GetQuizListReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -155,7 +167,7 @@ func (s *Service) GetQuizList(ctx *fiber.Ctx) error {
"'stop','start','draft', 'template','timeout','offlimit'")
}
res, cnt, err := s.dal.QuizRepo.GetQuizList(ctx.Context(),
res, cnt, err := r.dal.QuizRepo.GetQuizList(ctx.Context(),
quiz.GetQuizListDeps{
Limit: req.Limit,
Offset: req.Limit * req.Page,
@ -199,7 +211,11 @@ type UpdateQuizReq struct {
GroupId uint64 `json:"group_id"`
}
func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
type UpdateResp struct {
Updated uint64 `json:"updated"`
}
func (r *Quiz) UpdateQuiz(ctx *fiber.Ctx) error {
var req UpdateQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -241,7 +257,7 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing")
}
quiz, err := s.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId)
quiz, err := r.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -313,7 +329,7 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id))
if err := s.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil {
if err := r.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -341,7 +357,7 @@ type CopyQuizReq struct {
}
// CopyQuiz request handler for copy quiz
func (s *Service) CopyQuiz(ctx *fiber.Ctx) error {
func (r *Quiz) CopyQuiz(ctx *fiber.Ctx) error {
var req CopyQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -356,7 +372,7 @@ func (s *Service) CopyQuiz(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
quiz, err := s.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id)
quiz, err := r.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -374,7 +390,7 @@ type GetQuizHistoryReq struct {
}
// GetQuizHistory handler for history of quiz
func (s *Service) GetQuizHistory(ctx *fiber.Ctx) error {
func (r *Quiz) GetQuizHistory(ctx *fiber.Ctx) error {
var req GetQuizHistoryReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -388,7 +404,7 @@ func (s *Service) GetQuizHistory(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
history, err := s.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{
history, err := r.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{
Id: req.Id,
Limit: req.Limit,
Offset: req.Page * req.Limit,
@ -411,7 +427,7 @@ type DeactivateResp struct {
}
// DeleteQuiz handler for fake delete quiz
func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
func (r *Quiz) DeleteQuiz(ctx *fiber.Ctx) error {
var req DeactivateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -427,7 +443,7 @@ func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting is required")
}
deleted, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
deleted, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -443,7 +459,7 @@ func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
}
// ArchiveQuiz handler for archiving quiz
func (s *Service) ArchiveQuiz(ctx *fiber.Ctx) error {
func (r *Quiz) ArchiveQuiz(ctx *fiber.Ctx) error {
var req DeactivateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -458,7 +474,7 @@ func (s *Service) ArchiveQuiz(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for archive quiz is required")
}
archived, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
archived, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -472,7 +488,7 @@ type QuizMoveReq struct {
Qid, AccountID string
}
func (s *Service) QuizMove(ctx *fiber.Ctx) error {
func (r *Quiz) QuizMove(ctx *fiber.Ctx) error {
var req QuizMoveReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -482,7 +498,7 @@ func (s *Service) QuizMove(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid and accountID is required")
}
resp, err := s.dal.QuizRepo.QuizMove(ctx.Context(), req.Qid, req.AccountID)
resp, err := r.dal.QuizRepo.QuizMove(ctx.Context(), req.Qid, req.AccountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -490,7 +506,7 @@ func (s *Service) QuizMove(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (s *Service) TemplateCopy(ctx *fiber.Ctx) error {
func (r *Quiz) TemplateCopy(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
@ -509,7 +525,7 @@ func (s *Service) TemplateCopy(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid is required")
}
qizID, err := s.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
qizID, err := r.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
if err != nil {
fmt.Println("TEMPLERR", err)
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())

@ -0,0 +1,19 @@
package quiz
import "github.com/gofiber/fiber/v2"
func (r *Quiz) Register(router fiber.Router) {
router.Post("/create", r.CreateQuiz)
router.Post("/getList", r.GetQuizList)
router.Patch("/edit", r.UpdateQuiz)
router.Post("/copy", r.CopyQuiz)
router.Post("/history", r.GetQuizHistory)
router.Delete("/delete", r.DeleteQuiz)
router.Patch("/archive", r.ArchiveQuiz)
router.Post("/move", r.QuizMove)
router.Post("/template", r.TemplateCopy)
}
func (r *Quiz) Name() string {
return "quiz"
}

@ -1,16 +1,34 @@
package service
package result
import (
"bytes"
"github.com/gofiber/fiber/v2"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/repository/result"
"gitea.pena/SQuiz/core/tools"
"github.com/gofiber/fiber/v2"
"gitea.pena/SQuiz/core/internal/tools"
"strconv"
"time"
)
type Deps struct {
DAL *dal.DAL
S3Prefix string
}
type Result struct {
dal *dal.DAL
s3Prefix string
}
func NewResultController(deps Deps) *Result {
return &Result{
dal: deps.DAL,
s3Prefix: deps.S3Prefix,
}
}
type ReqExport struct {
To, From time.Time
New bool
@ -23,7 +41,7 @@ type ReqExportResponse struct {
Results []model.AnswerExport `json:"results"`
}
func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
func (r *Result) GetResultsByQuizID(ctx *fiber.Ctx) error {
payment := true // параметр для определения существования текущих привилегий юзера
accountID, ok := middleware.GetAccountId(ctx)
@ -42,7 +60,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -52,7 +70,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
}
}
results, totalCount, err := s.dal.ResultRepo.GetQuizResults(ctx.Context(), quizID, result.GetQuizResDeps{
results, totalCount, err := r.dal.ResultRepo.GetQuizResults(ctx.Context(), quizID, result.GetQuizResDeps{
To: req.To,
From: req.From,
New: req.New,
@ -71,7 +89,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
func (r *Result) DelResultByID(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
@ -83,7 +101,7 @@ func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid result ID format")
}
isOwner, err := s.dal.ResultRepo.CheckResultOwner(ctx.Context(), resultID, accountID)
isOwner, err := r.dal.ResultRepo.CheckResultOwner(ctx.Context(), resultID, accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -92,7 +110,7 @@ func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner of the result")
}
if err := s.dal.ResultRepo.SoftDeleteResultByID(ctx.Context(), resultID); err != nil {
if err := r.dal.ResultRepo.SoftDeleteResultByID(ctx.Context(), resultID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -104,7 +122,7 @@ type ReqSeen struct {
Answers []int64
}
func (s *Service) SetStatus(ctx *fiber.Ctx) error {
func (r *Result) SetStatus(ctx *fiber.Ctx) error {
var req ReqSeen
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
@ -115,7 +133,7 @@ func (s *Service) SetStatus(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
}
answers, err := s.dal.ResultRepo.CheckResultsOwner(ctx.Context(), req.Answers, accountID)
answers, err := r.dal.ResultRepo.CheckResultsOwner(ctx.Context(), req.Answers, accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -124,14 +142,14 @@ func (s *Service) SetStatus(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusNotAcceptable).SendString("could not update some answers because you don't have rights")
}
if err := s.dal.ResultRepo.UpdateAnswersStatus(ctx.Context(), accountID, answers); err != nil {
if err := r.dal.ResultRepo.UpdateAnswersStatus(ctx.Context(), accountID, answers); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(nil)
}
func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
func (r *Result) ExportResultsToCSV(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
@ -148,7 +166,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("invalid request body")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -159,17 +177,17 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
}
}
quiz, err := s.dal.QuizRepo.GetQuizById(ctx.Context(), accountID, quizID)
quiz, err := r.dal.QuizRepo.GetQuizById(ctx.Context(), accountID, quizID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get quiz")
}
questions, err := s.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
questions, err := r.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
}
answers, err := s.dal.ResultRepo.GetQuizResultsCSV(ctx.Context(), quizID, result.GetQuizResDeps{
answers, err := r.dal.ResultRepo.GetQuizResultsCSV(ctx.Context(), quizID, result.GetQuizResDeps{
To: req.To,
From: req.From,
New: req.New,
@ -182,7 +200,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
buffer := new(bytes.Buffer)
if err := tools.WriteDataToExcel(buffer, questions, answers, s.s3Prefix + quiz.Qid + "/"); err != nil {
if err := tools.WriteDataToExcel(buffer, questions, answers, r.s3Prefix+quiz.Qid+"/"); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
}
@ -192,7 +210,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
return ctx.Send(buffer.Bytes())
}
func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
func (r *Result) GetResultAnswers(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
@ -203,7 +221,7 @@ func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -214,11 +232,11 @@ func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
}
}
answers, err := s.dal.ResultRepo.GetResultAnswers(ctx.Context(), resultID)
answers, err := r.dal.ResultRepo.GetResultAnswers(ctx.Context(), resultID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get result answers")
}
sortedAnswers, err := s.dal.QuestionRepo.ForSortingResults(ctx.Context(), answers)
sortedAnswers, err := r.dal.QuestionRepo.ForSortingResults(ctx.Context(), answers)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed sort result answers")
}

@ -0,0 +1,15 @@
package result
import "github.com/gofiber/fiber/v2"
func (r *Result) Register(router fiber.Router) {
router.Post("/results/getResults/:quizId", r.GetResultsByQuizID)
router.Delete("/results/delete/:resultId", r.DelResultByID)
router.Patch("/result/seen", r.SetStatus)
router.Post("/results/:quizID/export", r.ExportResultsToCSV)
router.Get("/result/:resultID", r.GetResultAnswers)
}
func (r *Result) Name() string {
return ""
}

@ -0,0 +1,15 @@
package statistic
import "github.com/gofiber/fiber/v2"
func (r *Statistic) Register(router fiber.Router) {
router.Post("/statistic/:quizID/devices", r.GetDeviceStatistics)
router.Post("/statistic/:quizID/general", r.GetGeneralStatistics)
router.Post("/statistic/:quizID/questions", r.GetQuestionsStatistics)
router.Post("/statistic", r.AllServiceStatistics)
router.Get("/statistics/:quizID/pipelines", r.GetPipelinesStatistics)
}
func (r *Statistic) Name() string {
return ""
}

@ -1,17 +1,35 @@
package service
package statistic
import (
"github.com/gofiber/fiber/v2"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/repository/statistics"
"github.com/gofiber/fiber/v2"
"strconv"
)
type Deps struct {
DAL *dal.DAL
ChDAL *dal.ClickHouseDAL
}
type Statistic struct {
dal *dal.DAL
chDAL *dal.ClickHouseDAL
}
func NewStatisticController(deps Deps) *Statistic {
return &Statistic{
dal: deps.DAL,
chDAL: deps.ChDAL,
}
}
type DeviceStatReq struct {
From uint64 // временные границы выбора статистики
To uint64
}
func (s *Service) GetDeviceStatistics(ctx *fiber.Ctx) error {
func (r *Statistic) GetDeviceStatistics(ctx *fiber.Ctx) error {
quizIDStr := ctx.Params("quizID")
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
@ -24,7 +42,7 @@ func (s *Service) GetDeviceStatistics(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
deviceStats, err := s.dal.StatisticsRepo.GetDeviceStatistics(ctx.Context(), statistics.DeviceStatReq{
deviceStats, err := r.dal.StatisticsRepo.GetDeviceStatistics(ctx.Context(), statistics.DeviceStatReq{
QuizId: quizID,
From: req.From,
To: req.To,
@ -40,7 +58,7 @@ type GeneralStatsResp struct {
Open, Result, AvTime, Conversion map[uint64]uint64
}
func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
func (r *Statistic) GetGeneralStatistics(ctx *fiber.Ctx) error {
quizIDStr := ctx.Params("quizID")
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
if err != nil {
@ -52,7 +70,7 @@ func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
generalStats, err := s.dal.StatisticsRepo.GetGeneralStatistics(ctx.Context(), statistics.DeviceStatReq{
generalStats, err := r.dal.StatisticsRepo.GetGeneralStatistics(ctx.Context(), statistics.DeviceStatReq{
QuizId: quizID,
From: req.From,
To: req.To,
@ -64,7 +82,7 @@ func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(generalStats)
}
func (s *Service) GetQuestionsStatistics(ctx *fiber.Ctx) error {
func (r *Statistic) GetQuestionsStatistics(ctx *fiber.Ctx) error {
quizIDStr := ctx.Params("quizID")
quizID, err := strconv.ParseInt(quizIDStr, 0, 64)
if err != nil {
@ -73,10 +91,10 @@ func (s *Service) GetQuestionsStatistics(ctx *fiber.Ctx) error {
var req DeviceStatReq
if err := ctx.BodyParser(&req); err != nil {
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
questionsStats, err := s.dal.StatisticsRepo.GetQuestionsStatistics(ctx.Context(), statistics.DeviceStatReq{
questionsStats, err := r.dal.StatisticsRepo.GetQuestionsStatistics(ctx.Context(), statistics.DeviceStatReq{
QuizId: quizID,
From: req.From,
To: req.To,
@ -92,13 +110,13 @@ type StatisticReq struct {
From, To uint64 // временные границы выбора статистики
}
func (s *Service) AllServiceStatistics(ctx *fiber.Ctx) error {
func (r *Statistic) AllServiceStatistics(ctx *fiber.Ctx) error {
var req StatisticReq
if err := ctx.BodyParser(&req); err != nil {
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
allSvcStats, err := s.dal.StatisticsRepo.AllServiceStatistics(ctx.Context(), req.From, req.To)
allSvcStats, err := r.dal.StatisticsRepo.AllServiceStatistics(ctx.Context(), req.From, req.To)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
@ -106,10 +124,10 @@ func (s *Service) AllServiceStatistics(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(allSvcStats)
}
func (s *Service) GetPipelinesStatistics(ctx *fiber.Ctx) error {
func (r *Statistic) GetPipelinesStatistics(ctx *fiber.Ctx) error {
var req StatisticReq
if err := ctx.BodyParser(&req); err != nil {
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
quizIDStr := ctx.Params("quizID")
@ -118,7 +136,7 @@ func (s *Service) GetPipelinesStatistics(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
}
result, err := s.chDAL.StatisticClickRepo.GetPipelinesStatistics(ctx.Context(), quizID, req.From, req.To)
result, err := r.chDAL.StatisticClickRepo.GetPipelinesStatistics(ctx.Context(), quizID, req.From, req.To)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

@ -0,0 +1,14 @@
package telegram
import "github.com/gofiber/fiber/v2"
func (r *Telegram) Register(router fiber.Router) {
router.Get("/pool", r.GetPoolTgAccounts)
router.Post("/create", r.AddingTgAccount)
router.Delete("/:id", r.DeleteTgAccountByID)
router.Post("/setCode", r.SettingTgCode)
}
func (r *Telegram) Name() string {
return "telegram"
}

@ -0,0 +1,194 @@
package telegram
import (
"errors"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/pj_errors"
//"fmt"
"github.com/gofiber/fiber/v2"
// "github.com/rs/xid"
//"path/filepath"
// "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
// "gitea.pena/SQuiz/core/clients/telegram"
// "penahub.gitlab.yandexcloud.net/backend/tdlib/client"
"strconv"
)
type Deps struct {
DAL *dal.DAL
//TelegramClient *telegram.TelegramClient
}
type Telegram struct {
dal *dal.DAL
//telegramClient *telegram.TelegramClient
}
func NewTelegramController(deps Deps) *Telegram {
return &Telegram{
dal: deps.DAL,
//telegramClient: deps.TelegramClient,
}
}
type Message struct {
Type string `json:"type"`
Data string `json:"data"`
}
func (r *Telegram) GetPoolTgAccounts(ctx *fiber.Ctx) error {
allAccounts, err := r.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 (r *Telegram) 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))
// }
// }
return nil
}
func (r *Telegram) 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)
// }
return nil
// 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 (r *Telegram) 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 = r.dal.TgRepo.SoftDeleteTgAccount(ctx.Context(), id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.SendStatus(fiber.StatusOK)
}

@ -1,4 +1,4 @@
package rpc_service
package rpc_controllers
import (
"context"

@ -0,0 +1,24 @@
package initialize
import (
"context"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/core/internal/clients/auth"
)
type Clients struct {
AuthClient *auth.AuthClient
//TgClient *telegram.TelegramClient
}
func NewClients(ctx context.Context, cfg Config, pgDAL *dal.DAL) (*Clients, error) {
//tgClient, err := telegram.NewTelegramClient(ctx, pgDAL)
//if err != nil {
// return nil, err
//}
return &Clients{
//TgClient: tgClient,
AuthClient: auth.NewAuthClient(cfg.AuthMicroserviceURL),
}, nil
}

@ -0,0 +1,43 @@
package initialize
import (
"github.com/caarlos0/env/v8"
"github.com/joho/godotenv"
"log"
)
type Config struct {
LoggerProdMode bool `env:"IS_PROD_LOG" envDefault:"false"`
IsProd bool `env:"IS_PROD" envDefault:"false"`
ClientHttpURL string `env:"CLIENT_HTTP_URL" envDefault:"0.0.0.0:1488"`
GrpcURL string `env:"GRPC_URL" envDefault:"localhost:9000"`
PostgresURL string `env:"POSTGRES_URL" envDefault:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
ClickhouseURL string `env:"CLICKHOUSE_URL" envDefault:"tcp://10.8.0.15:9000/default?sslmode=disable"`
HubadminMicroserviceURL string `env:"HUBADMIN_MICROSERVICE_URL" envDefault:"http://localhost:8001/"`
AuthMicroserviceURL string `env:"AUTH_MICROSERVICE_URL" envDefault:"http://localhost:8000/"`
KafkaBrokers string `env:"KAFKA_BROKERS" envDefault:"localhost:9092"`
KafkaGroup string `env:"KAFKA_GROUP" envDefault:"mailnotifier"`
KafkaTopicNotifyer string `env:"KAFKA_TOPIC" envDefault:"test-topic"`
TrashLogHost string `env:"TRASH_LOG_HOST" envDefault:"localhost:7113"`
S3Prefix string `env:"S3_PREFIX"`
RedisHost string `env:"REDIS_HOST" envDefault:"localhost:6379"`
RedisPassword string `env:"REDIS_PASSWORD" envDefault:"admin"`
RedisDB uint64 `env:"REDIS_DB" envDefault:"2"`
CrtFile string `env:"CRT" envDefault:"server.crt"`
KeyFile string `env:"KEY" envDefault:"server.key"`
ServiceName string `env:"SERVICE_NAME" envDefault:"squiz"`
}
func LoadConfig() (*Config, error) {
if err := godotenv.Load(); err != nil {
log.Print("No .env file found")
}
var config Config
if err := env.Parse(&config); err != nil {
return nil, err
}
return &config, nil
}
const ModuleLogger = "core"

@ -0,0 +1,73 @@
package initialize
import (
"github.com/go-redis/redis/v8"
"gitea.pena/SQuiz/core/internal/brokers"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/account"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/question"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/quiz"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/result"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/statistic"
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/telegram"
"gitea.pena/SQuiz/core/internal/controllers/rpc_controllers"
)
type ControllerDeps struct {
Clients *Clients
DALs *DALs
Config Config
Producer *brokers.Producer
RedisClient *redis.Client
}
type Controller struct {
GRpcControllers GRpcControllers
HttpControllers HttpControllers
}
type GRpcControllers struct {
MailNotify *rpc_controllers.MailNotify
}
type HttpControllers struct {
Account *account.Account
Question *question.Question
Quiz *quiz.Quiz
Result *result.Result
Statistic *statistic.Statistic
Telegram *telegram.Telegram
}
func NewControllers(deps ControllerDeps) *Controller {
return &Controller{
GRpcControllers: GRpcControllers{
MailNotify: rpc_controllers.NewMailNotify(deps.DALs.PgDAL),
},
HttpControllers: HttpControllers{
Account: account.NewAccountController(account.Deps{
Dal: deps.DALs.PgDAL,
AuthClient: deps.Clients.AuthClient,
Producer: deps.Producer,
ServiceName: deps.Config.ServiceName,
RedisClient: deps.RedisClient,
}),
Question: question.NewQuestionController(question.Deps{
DAL: deps.DALs.PgDAL,
}),
Quiz: quiz.NewQuizController(quiz.Deps{
DAL: deps.DALs.PgDAL,
}),
Result: result.NewResultController(result.Deps{
DAL: deps.DALs.PgDAL,
S3Prefix: deps.Config.S3Prefix,
}),
Statistic: statistic.NewStatisticController(statistic.Deps{
DAL: deps.DALs.PgDAL,
ChDAL: deps.DALs.ChDAL,
}),
Telegram: telegram.NewTelegramController(telegram.Deps{
DAL: deps.DALs.PgDAL,
//TelegramClient: deps.Clients.TgClient,
}),
},
}
}

@ -0,0 +1,28 @@
package initialize
import (
"context"
"gitea.pena/SQuiz/common/dal"
)
type DALs struct {
PgDAL *dal.DAL
ChDAL *dal.ClickHouseDAL
}
func NewDALs(ctx context.Context, cfg Config) (*DALs, error) {
pgDal, err := dal.New(ctx, cfg.PostgresURL, nil)
if err != nil {
return nil, err
}
chDal, err := dal.NewClickHouseDAL(ctx, cfg.ClickhouseURL)
if err != nil {
return nil, err
}
return &DALs{
PgDAL: pgDal,
ChDAL: chDal,
}, nil
}

@ -0,0 +1,21 @@
package initialize
import (
"context"
"github.com/go-redis/redis/v8"
)
func Redis(ctx context.Context, cfg Config) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisHost,
Password: cfg.RedisPassword,
DB: int(cfg.RedisDB),
})
status := rdb.Ping(ctx)
if err := status.Err(); err != nil {
return nil, err
}
return rdb, nil
}

@ -2,15 +2,14 @@ package server
import (
"context"
"fmt"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
"go.uber.org/zap"
"google.golang.org/grpc"
"net"
"gitea.pena/SQuiz/core/initialize"
"gitea.pena/SQuiz/core/proto/notifyer"
"gitea.pena/SQuiz/core/internal/initialize"
"gitea.pena/SQuiz/core/internal/proto/notifyer"
"time"
)
@ -36,14 +35,7 @@ func NewGRPC(logger *zap.Logger) (*GRPC, error) {
}, nil
}
type DepsGrpcRun struct {
Host string
Port string
}
func (g *GRPC) Run(config DepsGrpcRun) {
connectionString := fmt.Sprintf("%s:%s", config.Host, config.Port)
func (g *GRPC) Run(connectionString string) {
g.logger.Info("Starting GRPC Server", zap.String("host", connectionString))
if err := g.listen(connectionString); err != nil && err != grpc.ErrServerStopped {
@ -58,7 +50,7 @@ func (g *GRPC) Stop(_ context.Context) error {
return nil
}
func (g *GRPC) Register(reg *initialize.RpcRegister) *GRPC {
func (g *GRPC) Register(reg initialize.GRpcControllers) *GRPC {
notifyer.RegisterQuizServiceServer(g.grpc, reg.MailNotify)
// another
return g

@ -0,0 +1,73 @@
package http
import (
"context"
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/PenaSide/hlog"
"gitea.pena/SQuiz/common/middleware"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
type ServerConfig struct {
Logger *zap.Logger
Controllers []Controller
Hlogger hlog.Logger
}
type Server struct {
Logger *zap.Logger
Controllers []Controller
app *fiber.App
}
func NewServer(config ServerConfig) *Server {
app := fiber.New()
app.Use(middleware.JWTAuth())
app.Use(log_mw.ContextLogger(config.Hlogger))
//app.Get("/liveness", healthchecks.Liveness)
//app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
s := &Server{
Logger: config.Logger,
Controllers: config.Controllers,
app: app,
}
s.registerRoutes()
return s
}
func (s *Server) Start(addr string) error {
if err := s.app.Listen(addr); err != nil {
s.Logger.Error("Failed to start server", zap.Error(err))
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.app.Shutdown()
}
func (s *Server) registerRoutes() {
for _, c := range s.Controllers {
router := s.app.Group(c.Name())
c.Register(router)
}
}
type Controller interface {
Register(router fiber.Router)
Name() string
}
func (s *Server) ListRoutes() {
fmt.Println("Registered routes:")
for _, stack := range s.app.Stack() {
for _, route := range stack {
fmt.Printf("%s %s\n", route.Method, route.Path)
}
}
}

434
internal/tools/tools.go Normal file

@ -0,0 +1,434 @@
package tools
import (
"encoding/json"
"fmt"
"gitea.pena/SQuiz/common/model"
"github.com/xuri/excelize/v2"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
)
const (
bucketImages = "squizimages"
bucketFonts = "squizfonts"
bucketScripts = "squizscript"
bucketStyle = "squizstyle"
bucketAnswers = "squizanswer"
)
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) 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, mapQueRes := prepareHeaders(questions)
headers = append([]string{"Дата и время"}, headers...)
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)
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, "Результат")
return headers, mapQueRes
}
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
} else {
standart[answer.Session] = append(standart[answer.Session], answer)
}
}
return standart, results
}
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].CreatedAt.Format("2006-01-02 15:04:05")); err != nil {
fmt.Println(err.Error())
}
if err := file.SetCellValue(sheet, "B"+strconv.Itoa(row), results[session].Content); err != nil {
fmt.Println(err.Error())
}
count := 3
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())
}
}
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
}
} 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())
//}
}
} 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
}
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 {
left := 0
right := len(answers) - 1
for left <= right {
mid := left + (right-left)/2
if answers[mid].QuestionId == questionID {
return mid
} else if answers[mid].QuestionId < questionID {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
func FileSearch(content string) string {
if strings.Contains(content, bucketImages) {
return FileType(content)
} else if strings.Contains(content, bucketFonts) {
return FileType(content)
} else if strings.Contains(content, bucketScripts) {
return FileType(content)
} else if strings.Contains(content, bucketStyle) {
return FileType(content)
} else if strings.Contains(content, bucketAnswers) {
return FileType(content)
}
return "Text"
}
func FileType(filename string) string {
parts := strings.Split(filename, ".")
extension := parts[len(parts)-1]
switch extension {
case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico":
return "Image"
default:
return "File"
}
}
func downloadImage(url string) (*excelize.Picture, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer func() {
if derr := resp.Body.Close(); derr != nil {
fmt.Printf("error close response body in downloadImage: %v", derr)
}
}()
imgData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ext := filepath.Ext(url)
if ext == "" {
contentType := resp.Header.Get("Content-Type")
switch {
case strings.HasPrefix(contentType, "image/jpeg"):
ext = ".jpg"
case strings.HasPrefix(contentType, "image/png"):
ext = ".png"
default:
ext = ".png"
}
}
pic := &excelize.Picture{
Extension: ext,
File: imgData,
Format: &excelize.GraphicOptions{
AutoFit: true,
Positioning: "oneCell",
},
}
return pic, nil
}
func ToAlphaString(col int) string {
var result string
for col > 0 {
col--
result = string(rune('A'+col%26)) + result
col /= 26
}
return result
}
func ExtractImageURL(htmlContent string) string {
re := regexp.MustCompile(`(?:<img[^>]*src="([^"]+)"[^>]*>)|(?:<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>)|(?:<tr[^>]*>.*?<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>.*?</tr>)|(?:<a[^>]*\s+download[^>]*>([^<]+)<\/a>)`)
matches := re.FindAllStringSubmatch(htmlContent, -1)
for _, match := range matches {
for i := 1; i < len(match); i++ {
if match[i] != "" {
return strings.TrimSpace(match[i])
}
}
}
return htmlContent
}

@ -0,0 +1,107 @@
package workers
import (
"context"
"gitea.pena/SQuiz/common/dal"
"github.com/go-redis/redis/v8"
"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
// }
//}
}

10
main.go

@ -1,10 +0,0 @@
package main
import (
"github.com/skeris/appInit"
"gitea.pena/SQuiz/core/app"
)
func main() {
appInit.Initialize(app.New, app.Options{})
}

37
pkg/closer/closer.go Normal file

@ -0,0 +1,37 @@
package closer
import (
"context"
)
type Closer interface {
Close(ctx context.Context) error
}
type CloserFunc func(ctx context.Context) error
func (cf CloserFunc) Close(ctx context.Context) error {
return cf(ctx)
}
type CloserGroup struct {
closers []Closer
}
func NewCloserGroup() *CloserGroup {
return &CloserGroup{}
}
func (cg *CloserGroup) Add(c Closer) {
cg.closers = append(cg.closers, c)
}
func (cg *CloserGroup) Call(ctx context.Context) error {
var closeErr error
for i := len(cg.closers) - 1; i >= 0; i-- {
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
closeErr = err
}
}
return closeErr
}

@ -1,24 +0,0 @@
-- Drop indexes
DROP INDEX IF EXISTS subquizes;
DROP INDEX IF EXISTS birthtime;
DROP INDEX IF EXISTS groups;
DROP INDEX IF EXISTS timeouted;
DROP INDEX IF EXISTS active ON quiz;
DROP INDEX IF EXISTS questiontype;
DROP INDEX IF EXISTS required;
DROP INDEX IF EXISTS relation;
DROP INDEX IF EXISTS active ON question;
-- Drop tables
DROP TABLE IF EXISTS privileges;
DROP TABLE IF EXISTS answer;
DROP TABLE IF EXISTS question;
DROP TABLE IF EXISTS quiz;
DROP TABLE IF EXISTS account;
-- Drop types
DO $$
BEGIN
DROP TYPE IF EXISTS question_type;
DROP TYPE IF EXISTS quiz_status;
END$$;

@ -1,120 +0,0 @@
-- Create types
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'question_type') THEN
CREATE TYPE question_type AS ENUM (
'variant',
'images',
'varimg',
'emoji',
'text',
'select',
'date',
'number',
'file',
'page',
'rating'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'quiz_status') THEN
CREATE TYPE quiz_status AS ENUM (
'draft',
'template',
'stop',
'start',
'timeout',
'offlimit'
);
END IF;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
END$$;
-- Create tables
CREATE TABLE IF NOT EXISTS account (
id UUID PRIMARY KEY,
user_id VARCHAR(24),
email VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN DEFAULT false
);
CREATE TABLE IF NOT EXISTS quiz (
id bigserial UNIQUE NOT NULL PRIMARY KEY,
qid uuid DEFAULT uuid_generate_v4(),
accountid varchar(30) NOT NULL,
deleted boolean DEFAULT false,
archived boolean DEFAULT false,
fingerprinting boolean DEFAULT false,
repeatable boolean DEFAULT false,
note_prevented boolean DEFAULT false,
mail_notifications boolean DEFAULT false,
unique_answers boolean DEFAULT false,
super boolean DEFAULT false,
group_id bigint DEFAULT 0,
name varchar(280),
description text,
config text,
status quiz_status DEFAULT 'draft',
limit_answers integer DEFAULT 0,
due_to integer DEFAULT 0,
time_of_passing integer DEFAULT 0,
pausable boolean DEFAULT false,
version smallint DEFAULT 0,
version_comment text DEFAULT '',
parent_ids integer[],
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp DEFAULT CURRENT_TIMESTAMP,
questions_count integer DEFAULT 0,
answers_count integer DEFAULT 0,
average_time_passing integer DEFAULT 0
);
CREATE TABLE IF NOT EXISTS question (
id bigserial UNIQUE NOT NULL PRIMARY KEY,
quiz_id bigint NOT NULL,
title varchar(512) NOT NULL,
description text,
questiontype question_type DEFAULT 'text',
required boolean DEFAULT false,
deleted boolean DEFAULT false,
page smallint DEFAULT 0,
content text,
version smallint DEFAULT 0,
parent_ids integer[],
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT quiz_relation FOREIGN KEY(quiz_id) REFERENCES quiz(id)
);
CREATE TABLE IF NOT EXISTS answer (
id bigserial UNIQUE NOT NULL PRIMARY KEY,
content text,
quiz_id bigint NOT NULL REFERENCES quiz(id),
question_id bigint NOT NULL REFERENCES question(id),
fingerprint varchar(1024),
session varchar(20),
created_at timestamp DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS privileges (
id SERIAL PRIMARY KEY,
privilegeID VARCHAR(50),
account_id UUID,
privilege_name VARCHAR(255),
amount INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES account (id)
);
-- Create indexes
CREATE INDEX IF NOT EXISTS active ON question(deleted) WHERE deleted=false;
CREATE INDEX IF NOT EXISTS relation ON question(quiz_id DESC);
CREATE INDEX IF NOT EXISTS required ON question(required DESC);
CREATE INDEX IF NOT EXISTS questiontype ON question(questiontype);
CREATE INDEX IF NOT EXISTS active ON quiz(deleted, archived, status) WHERE deleted = false AND archived = false AND status = 'start';
CREATE INDEX IF NOT EXISTS timeouted ON quiz(due_to DESC) WHERE deleted = false AND due_to <> 0 AND status <> 'timeout';
CREATE INDEX IF NOT EXISTS groups ON quiz(super) WHERE super = true;
CREATE INDEX IF NOT EXISTS birthtime ON quiz(created_at DESC);
CREATE INDEX IF NOT EXISTS subquizes ON quiz(group_id DESC) WHERE group_id <> 0;

@ -1 +0,0 @@
ALTER TABLE answer DROP COLUMN IF EXISTS result;

@ -1 +0,0 @@
ALTER TABLE answer ADD COLUMN result BOOLEAN DEFAULT FALSE;

@ -1 +0,0 @@
ALTER TABLE quiz DROP COLUMN IF EXISTS sessions_count;

@ -1 +0,0 @@
ALTER TABLE quiz ADD COLUMN sessions_count integer;

@ -1,2 +0,0 @@
ALTER TABLE quiz DROP COLUMN IF EXISTS new;
ALTER TABLE quiz DROP COLUMN IF EXISTS deleted;

@ -1,2 +0,0 @@
ALTER TABLE answer ADD COLUMN new BOOLEAN DEFAULT TRUE;
ALTER TABLE answer ADD COLUMN deleted BOOLEAN DEFAULT FALSE;

@ -1,2 +0,0 @@
ALTER TABLE answer DROP COLUMN IF EXISTS email;
DROP INDEX IF EXISTS answer_email_unique_idx;

@ -1,2 +0,0 @@
ALTER TABLE answer ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT '';
CREATE UNIQUE INDEX IF NOT EXISTS answer_email_unique_idx ON answer (quiz_id, email) WHERE email <> '';

@ -1,6 +0,0 @@
ALTER TABLE answer
DROP COLUMN device_type,
DROP COLUMN device,
DROP COLUMN os,
DROP COLUMN browser,
DROP COLUMN ip;

@ -1,6 +0,0 @@
ALTER TABLE answer
ADD COLUMN device_type VARCHAR(50) NOT NULL DEFAULT '',
ADD COLUMN device VARCHAR(100) NOT NULL DEFAULT '',
ADD COLUMN os VARCHAR(100) NOT NULL DEFAULT '',
ADD COLUMN browser VARCHAR(100) NOT NULL DEFAULT '',
ADD COLUMN ip VARCHAR(50) NOT NULL DEFAULT '';

@ -1,2 +0,0 @@
ALTER TABLE answer
DROP COLUMN start;

@ -1,2 +0,0 @@
ALTER TABLE answer
ADD COLUMN start BOOLEAN NOT NULL DEFAULT FALSE;

@ -1,4 +0,0 @@
ALTER TABLE answer
ALTER COLUMN device TYPE VARCHAR(100),
ALTER COLUMN os TYPE VARCHAR(100),
ALTER COLUMN browser TYPE VARCHAR(100);

@ -1,4 +0,0 @@
ALTER TABLE answer
ALTER COLUMN device TYPE VARCHAR(1024),
ALTER COLUMN os TYPE VARCHAR(1024),
ALTER COLUMN browser TYPE VARCHAR(1024);

@ -1,2 +0,0 @@
ALTER TABLE quiz
ALTER COLUMN name TYPE VARCHAR(280);

@ -1,2 +0,0 @@
ALTER TABLE quiz
ALTER COLUMN name TYPE VARCHAR(1024);

@ -1,238 +0,0 @@
package service
import (
"database/sql"
"errors"
"github.com/gofiber/fiber/v2"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/pj_errors"
"gitea.pena/SQuiz/core/brokers"
"gitea.pena/SQuiz/core/models"
"time"
)
type CreateAccountReq struct {
UserID string `json:"userId"`
}
type CreateAccountResp struct {
CreatedAccount model.Account `json:"created_account"`
}
type DeleteAccountResp struct {
DeletedAccountID string `json:"account_Id"`
}
type GetPrivilegeByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDResp struct {
DeletedAccountUserID string `json:"userId"`
}
type GetAccountsReq struct {
Limit uint64 `json:"limit"`
Page uint64 `json:"page"`
}
type GetAccountsResp struct {
Count uint64 `json:"count"`
Items []model.Account `json:"items"`
}
// getCurrentAccount обработчик для получения текущего аккаунта
func (s *Service) getCurrentAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil && err != sql.ErrNoRows {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
//TODO: fix this later
if account.ID == "" {
return ctx.Status(fiber.StatusNotFound).SendString("no account")
}
return ctx.Status(fiber.StatusOK).JSON(account)
}
// createAccount обработчик для создания нового аккаунта
func (s *Service) createAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
hlogger := log_mw.ExtractLogger(ctx)
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil && err != sql.ErrNoRows {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID != "" {
return ctx.Status(fiber.StatusConflict).SendString("user with this ID already exists")
}
email, err := s.authClient.GetUserEmail(accountID)
if err != nil {
return err
}
newAccount := model.Account{
UserID: accountID,
CreatedAt: time.Now(),
Email: email,
Deleted: false,
Privileges: map[string]model.ShortPrivilege{
"quizUnlimTime": {
PrivilegeID: "quizUnlimTime",
PrivilegeName: "Безлимит Опросов",
Amount: 14,
CreatedAt: time.Now(),
},
},
}
createdAcc, err := s.dal.AccountRepo.CreateAccount(ctx.Context(), &newAccount)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
hlogger.Emit(models.InfoAccountCreated{
CtxUserID: accountID,
CtxAccountID: createdAcc.ID,
})
err = s.producer.ToMailNotify(ctx.Context(), brokers.Message{
AccountID: accountID,
Email: email,
ServiceKey: s.serviceName,
SendAt: time.Now(),
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(CreateAccountResp{
CreatedAccount: newAccount,
})
}
// deleteAccount обработчик для удаления текущего аккаунта
func (s *Service) deleteAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), account.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountResp{
DeletedAccountID: accountID,
})
}
// getPrivilegeByUserID обработчик для получения привилегий аккаунта по ID пользователя
func (s *Service) getPrivilegeByUserID(ctx *fiber.Ctx) error {
var req GetPrivilegeByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
privilege, err := s.dal.AccountRepo.GetPrivilegesByAccountID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(privilege)
}
// deleteAccountByUserID обработчик для удаления аккаунта по ID пользователя
func (s *Service) deleteAccountByUserID(ctx *fiber.Ctx) error {
var req DeleteAccountByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID == "" {
return ctx.Status(fiber.StatusInternalServerError).SendString("user with this ID not found")
}
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), existingAccount.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountByUserIDResp{
DeletedAccountUserID: req.UserID,
})
}
// getAccounts обработчик для получения списка аккаунтов с пагинацией
func (s *Service) getAccounts(ctx *fiber.Ctx) error {
var req GetAccountsReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
_, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
accounts, totalCount, err := s.dal.AccountRepo.GetAccounts(ctx.Context(), req.Limit, req.Page)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
response := GetAccountsResp{
Count: totalCount,
Items: accounts,
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (s *Service) ManualDone(ctx *fiber.Ctx) error {
var req struct {
Id string `json:"id"`
}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("User id is required")
}
err := s.dal.AccountRepo.ManualDone(ctx.Context(), req.Id)
if err != nil {
if errors.Is(err, pj_errors.ErrNotFound) {
return ctx.Status(fiber.StatusNotFound).SendString("user don't have this privilege")
}
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}

@ -1,89 +0,0 @@
package service
import (
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/core/brokers"
"gitea.pena/SQuiz/core/clients/auth"
"github.com/gofiber/fiber/v2"
)
// 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
s3Prefix string
producerGigaChat *brokers.Producer
}
type Deps struct {
Dal *dal.DAL
AuthClient *auth.AuthClient
Producer *brokers.Producer
ServiceName string
ChDAL *dal.ClickHouseDAL
S3Prefix string
ProducerGigaChat *brokers.Producer
}
func New(deps Deps) *Service {
return &Service{
dal: deps.Dal,
authClient: deps.AuthClient,
producer: deps.Producer,
serviceName: deps.ServiceName,
chDAL: deps.ChDAL,
s3Prefix: deps.S3Prefix,
}
}
// Register is a function for add handlers of service to external multiplexer
func (s *Service) Register(app *fiber.App) {
// quiz manipulating handlers
app.Post("/quiz/create", s.CreateQuiz)
app.Post("/quiz/getList", s.GetQuizList)
app.Patch("/quiz/edit", s.UpdateQuiz)
app.Post("/quiz/copy", s.CopyQuiz)
app.Post("/quiz/history", s.GetQuizHistory)
app.Delete("/quiz/delete", s.DeleteQuiz)
app.Patch("/quiz/archive", s.ArchiveQuiz)
app.Post("/quiz/move", s.QuizMove)
app.Post("/quiz/template", s.TemplateCopy)
app.Post("/quiz/:quizID/auditory", s.CreateQuizAuditory)
app.Get("/quiz/:quizID/auditory", s.GetQuizAuditory)
app.Delete("/quiz/:quizID/auditory", s.DeleteQuizAuditory)
// question manipulating handlers
app.Post("/question/create", s.CreateQuestion)
app.Post("/question/getList", s.GetQuestionList)
app.Patch("/question/edit", s.UpdateQuestion)
app.Post("/question/copy", s.CopyQuestion)
app.Post("/question/history", s.GetQuestionHistory)
app.Delete("/question/delete", s.DeleteQuestion)
// account handlers
app.Get("/account/get", s.getCurrentAccount)
app.Post("/account/create", s.createAccount)
app.Delete("/account/delete", s.deleteAccount)
app.Get("/accounts", s.getAccounts)
app.Get("/privilege/:userId", s.getPrivilegeByUserID)
app.Delete("/account/:userId", s.deleteAccountByUserID)
app.Post("/account/manualdone", s.ManualDone)
// result handlers
app.Post("/results/getResults/:quizId", s.GetResultsByQuizID)
app.Delete("/results/delete/:resultId", s.DelResultByID)
app.Patch("/result/seen", s.SetStatus)
app.Post("/results/:quizID/export", s.ExportResultsToCSV)
app.Get("/result/:resultID", s.GetResultAnswers)
// statistics handlers
app.Post("/statistic/:quizID/devices", s.GetDeviceStatistics)
app.Post("/statistic/:quizID/general", s.GetGeneralStatistics)
app.Post("/statistic/:quizID/questions", s.GetQuestionsStatistics)
app.Post("/statistic", s.AllServiceStatistics)
app.Get("/statistics/:quizID/pipelines", s.GetPipelinesStatistics)
}

@ -2,11 +2,11 @@ package tests
import (
"context"
"gitea.pena/SQuiz/core/internal/brokers"
"gitea.pena/SQuiz/core/internal/initialize"
"github.com/pioz/faker"
"go.uber.org/zap"
"log"
"gitea.pena/SQuiz/core/brokers"
"gitea.pena/SQuiz/core/initialize"
"testing"
"time"
)

@ -1,10 +1,10 @@
package tests
import (
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"gitea.pena/PenaSide/common/privilege"
"gitea.pena/SQuiz/common/model"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"testing"
)

@ -2,12 +2,11 @@ package tests
import (
_ "embed"
"gitea.pena/SQuiz/common/clients"
"gitea.pena/SQuiz/common/model"
"github.com/gofiber/fiber/v2"
"github.com/pioz/faker"
"github.com/stretchr/testify/assert"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/worker/answerwc"
"gitea.pena/SQuiz/worker/clients/mailclient"
"testing"
"time"
)
@ -19,16 +18,16 @@ var toClientTemplate string
var reminderTemplate string
func TestProcessMessageToSMTP(t *testing.T) {
clientDeps := mailclient.ClientDeps{
Host: "connect.mailclient.bz",
Port: "587",
Sender: "skeris@mailing.pena.digital",
Auth: &mailclient.PlainAuth{Username: "kotilion.95@gmail.com", Password: "vWwbCSg4bf0p"},
clientDeps := clients.Deps{
SmtpHost: "connect.mailclient.bz",
SmtpPort: "587",
SmtpSender: "skeris@mailing.pena.digital",
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
FiberClient: &fiber.Client{},
SmtpApiUrl: "",
}
client := mailclient.NewClient(clientDeps)
client := clients.NewSmtpClient(clientDeps)
recipient := "mullinp@internet.ru"
subject := "Test"

@ -111,7 +111,11 @@ func registerUser(login string) *jwt.Token {
if err != nil {
panic(err)
}
defer resp.Body.Close()
defer func() {
if derr := resp.Body.Close(); derr != nil {
fmt.Printf("error close response body in registerUser: %v", derr)
}
}()
bytes, err := io.ReadAll(resp.Body)
if err != nil {

BIN
tools/migrate Executable file

Binary file not shown.

@ -1,449 +0,0 @@
package tools
import (
"encoding/json"
"fmt"
"github.com/xuri/excelize/v2"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"gitea.pena/SQuiz/common/model"
"regexp"
"sort"
"strconv"
"strings"
"sync"
)
const (
bucketImages = "squizimages"
bucketFonts = "squizfonts"
bucketScripts = "squizscript"
bucketStyle = "squizstyle"
bucketAnswers = "squizanswer"
)
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) 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{"Данные респондента"}
headers = append([]string{"Дата и время"}, headers...)
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
}
}
sort.Slice(answers, func(i, j int) bool {
return answers[i].QuestionId < answers[j].QuestionId
})
// мапа для хранения обычных ответов респондентов
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)
}
}
processSession := func(session string, response []model.Answer, 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].CreatedAt.Format("2006-01-02 15:04:05")); err != nil {
fmt.Println(err.Error())
}
if err := file.SetCellValue(sheet, "B"+strconv.Itoa(row), results[session].Content); err != nil {
fmt.Println(err.Error())
}
count := 3
for _, q := range questions {
if !q.Deleted && q.Type != model.TypeResult {
index := binarySearch(response, q.Id)
if index != -1 {
cell := ToAlphaString(count) + strconv.Itoa(row)
tipe := FileSearch(response[index].Content)
noAccept := make(map[string]struct{})
todoMap := make(map[string]string)
if tipe != "Text" && q.Type == model.TypeImages || q.Type == model.TypeVarImages {
var res model.ImageContent
err := json.Unmarshal([]byte(response[index].Content), &res)
if err != nil {
res.Image = response[index].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())
}
file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
file.SetRowHeight(sheet, row, 150)
if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
fmt.Println(err.Error())
}
noAccept[response[index].Content] = struct{}{}
} else {
todoMap[response[index].Content] = cell
}
} else {
todoMap[response[index].Content] = cell
}
} else if q.Type == model.TypeFile {
urle := response[index].Content
if urle != "" && !strings.HasPrefix(urle, "https") {
urle = strings.ReplaceAll(s3Prefix,bucketImages, bucketAnswers) + fmt.Sprint(q.Id)+ "/" + urle
}
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[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 {
fmt.Println(err.Error())
}
}
}
} else {
cell := ToAlphaString(count) + strconv.Itoa(row)
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
fmt.Println(err.Error())
}
}
count++
}
}
index := binarySearch(response, results[session].QuestionId)
content := response[index].Content
score , err := strconv.ParseInt(content, 10, 64)
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
if err != nil {
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
fmt.Println(err.Error())
}
} else {
if err := file.SetCellValue(sheet, cell, score); err != nil {
fmt.Println(err.Error())
}
}
}
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
}
return nil
}
func binarySearch(answers []model.Answer, questionID uint64) int {
left := 0
right := len(answers) - 1
for left <= right {
mid := left + (right-left)/2
if answers[mid].QuestionId == questionID {
return mid
} else if answers[mid].QuestionId < questionID {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
func FileSearch(content string) string {
if strings.Contains(content, bucketImages) {
return FileType(content)
} else if strings.Contains(content, bucketFonts) {
return FileType(content)
} else if strings.Contains(content, bucketScripts) {
return FileType(content)
} else if strings.Contains(content, bucketStyle) {
return FileType(content)
} else if strings.Contains(content, bucketAnswers) {
return FileType(content)
}
return "Text"
}
func FileType(filename string) string {
parts := strings.Split(filename, ".")
extension := parts[len(parts)-1]
switch extension {
case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico":
return "Image"
default:
return "File"
}
}
func downloadImage(url string) (*excelize.Picture, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
imgData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ext := filepath.Ext(url)
if ext == "" {
contentType := resp.Header.Get("Content-Type")
switch {
case strings.HasPrefix(contentType, "image/jpeg"):
ext = ".jpg"
case strings.HasPrefix(contentType, "image/png"):
ext = ".png"
default:
ext = ".png"
}
}
pic := &excelize.Picture{
Extension: ext,
File: imgData,
Format: &excelize.GraphicOptions{
AutoFit: true,
Positioning: "oneCell",
},
}
return pic, nil
}
func ToAlphaString(col int) string {
var result string
for col > 0 {
col--
result = string(rune('A'+col%26)) + result
col /= 26
}
return result
}
func ExtractImageURL(htmlContent string) string {
re := regexp.MustCompile(`(?:<img[^>]*src="([^"]+)"[^>]*>)|(?:<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>)|(?:<tr[^>]*>.*?<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>.*?</tr>)|(?:<a[^>]*\s+download[^>]*>([^<]+)<\/a>)`)
matches := re.FindAllStringSubmatch(htmlContent, -1)
for _, match := range matches {
for i := 1; i < len(match); i++ {
if match[i] != "" {
return strings.TrimSpace(match[i])
}
}
}
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
//}