From cc1bc6b6d56fc8da3a8bdc34ff93e5968b269046 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 27 Mar 2025 11:54:10 +0300 Subject: [PATCH 01/44] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- Makefile | 4 ++ cmd/algobot/main.go | 20 +++++++++ cmd/main.go | 14 +++--- config/dev.yaml | 8 ++++ go.mod | 6 ++- go.sum | 7 +++ internal/config/config.go | 43 +++++++++++++++++++ .../clients/backoffice.go | 2 +- .../clients/webClient.go | 0 .../config/keyboards.go | 0 {internal => internal_old}/config/texts.go | 0 .../callbackHandlers/changeNotification.go | 8 ++-- .../callbackHandlers/closeLesson.go | 8 ++-- .../callbackHandlers/getCredentials.go | 4 +- .../callbackHandlers/openLesson.go | 8 ++-- .../callbackHandlers/refreshGroups.go | 8 ++-- .../callbackHandlers/setCookie.go | 6 +-- .../contextHandlers/defaultHandler/handler.go | 0 .../handlersHolders/DefaultCBHolder.go | 8 ++-- .../handlersHolders/DefaultHolder.go | 8 ++-- .../handlersHolders/SendingCookie.go | 8 ++-- .../handlersHolders/chattingAI.go | 8 ++-- .../contextHandlers/handlersHolders/holder.go | 4 +- .../contextHandlers/onCallback.go | 10 ++--- .../contextHandlers/onText.go | 10 ++--- .../textHandlers/chattingAi/AnyMessage.go | 8 ++-- .../textHandlers/chattingAi/ClearHistory.go | 6 +-- .../textHandlers/chattingAi/backAction.go | 4 +- .../textHandlers/defaultState/absentKids.go | 8 ++-- .../textHandlers/defaultState/aiChat.go | 6 +-- .../textHandlers/defaultState/missingKids.go | 10 ++--- .../textHandlers/defaultState/myGroups.go | 12 +++--- .../textHandlers/defaultState/settings.go | 6 +-- .../textHandlers/defaultState/start.go | 2 +- .../defaultState/startWithPayload.go | 8 ++-- .../sendingCookieState/rejectAction.go | 4 +- .../sendingCookieState/sendCookie.go | 8 ++-- {internal => internal_old}/domain/domain.go | 0 {internal => internal_old}/domain/sqlite3.go | 2 +- {internal => internal_old}/error/error.go | 0 {internal => internal_old}/helpers/group.go | 4 +- .../helpers/group_test.go | 2 +- .../helpers/logError.go | 0 .../middleware/logger.go | 0 .../middleware/register.go | 8 ++-- {internal => internal_old}/models/models.go | 4 +- .../models/startPayload.go | 0 .../schedulers/message.go | 4 +- {internal => internal_old}/serdes/simple.go | 2 +- .../service/AIService.go | 2 +- .../service/DefaultService.go | 10 ++--- {internal => internal_old}/service/service.go | 2 +- .../stateMachine/memory.go | 0 .../stateMachine/stateMachine.go | 0 tests/clients/backoffice_test.go | 2 +- tests/domain/domain_test.go | 4 +- tests/handlers/chattingAI/chattingAI_test.go | 8 ++-- .../defaultState/CallbackHandler_test.go | 8 ++-- .../defaultState/DefaultHandler_test.go | 16 +++---- .../sendCookieState/sendCookieHandler_test.go | 8 ++-- tests/mocks/AIService_mock.go | 2 +- tests/mocks/MockStateMachine.go | 2 +- tests/mocks/mockDomain.go | 2 +- tests/mocks/mockWebClient.go | 2 +- tests/mocks/newMockService.go | 2 +- tests/scheduler/message_test.go | 4 +- tests/services/service_test.go | 10 ++--- tests/stateMachine_test.go | 2 +- 69 files changed, 243 insertions(+), 156 deletions(-) create mode 100644 cmd/algobot/main.go create mode 100644 config/dev.yaml create mode 100644 internal/config/config.go rename {internal => internal_old}/clients/backoffice.go (99%) rename {internal => internal_old}/clients/webClient.go (100%) rename {internal => internal_old}/config/keyboards.go (100%) rename {internal => internal_old}/config/texts.go (100%) rename {internal => internal_old}/contextHandlers/callbackHandlers/changeNotification.go (84%) rename {internal => internal_old}/contextHandlers/callbackHandlers/closeLesson.go (90%) rename {internal => internal_old}/contextHandlers/callbackHandlers/getCredentials.go (94%) rename {internal => internal_old}/contextHandlers/callbackHandlers/openLesson.go (90%) rename {internal => internal_old}/contextHandlers/callbackHandlers/refreshGroups.go (86%) rename {internal => internal_old}/contextHandlers/callbackHandlers/setCookie.go (85%) rename {internal => internal_old}/contextHandlers/defaultHandler/handler.go (100%) rename {internal => internal_old}/contextHandlers/handlersHolders/DefaultCBHolder.go (81%) rename {internal => internal_old}/contextHandlers/handlersHolders/DefaultHolder.go (80%) rename {internal => internal_old}/contextHandlers/handlersHolders/SendingCookie.go (75%) rename {internal => internal_old}/contextHandlers/handlersHolders/chattingAI.go (77%) rename {internal => internal_old}/contextHandlers/handlersHolders/holder.go (62%) rename {internal => internal_old}/contextHandlers/onCallback.go (88%) rename {internal => internal_old}/contextHandlers/onText.go (88%) rename {internal => internal_old}/contextHandlers/textHandlers/chattingAi/AnyMessage.go (86%) rename {internal => internal_old}/contextHandlers/textHandlers/chattingAi/ClearHistory.go (86%) rename {internal => internal_old}/contextHandlers/textHandlers/chattingAi/backAction.go (88%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/absentKids.go (93%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/aiChat.go (86%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/missingKids.go (94%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/myGroups.go (90%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/settings.go (93%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/start.go (91%) rename {internal => internal_old}/contextHandlers/textHandlers/defaultState/startWithPayload.go (97%) rename {internal => internal_old}/contextHandlers/textHandlers/sendingCookieState/rejectAction.go (89%) rename {internal => internal_old}/contextHandlers/textHandlers/sendingCookieState/sendCookie.go (86%) rename {internal => internal_old}/domain/domain.go (100%) rename {internal => internal_old}/domain/sqlite3.go (99%) rename {internal => internal_old}/error/error.go (100%) rename {internal => internal_old}/helpers/group.go (96%) rename {internal => internal_old}/helpers/group_test.go (98%) rename {internal => internal_old}/helpers/logError.go (100%) rename {internal => internal_old}/middleware/logger.go (100%) rename {internal => internal_old}/middleware/register.go (86%) rename {internal => internal_old}/models/models.go (95%) rename {internal => internal_old}/models/startPayload.go (100%) rename {internal => internal_old}/schedulers/message.go (95%) rename {internal => internal_old}/serdes/simple.go (94%) rename {internal => internal_old}/service/AIService.go (98%) rename {internal => internal_old}/service/DefaultService.go (98%) rename {internal => internal_old}/service/service.go (97%) rename {internal => internal_old}/stateMachine/memory.go (100%) rename {internal => internal_old}/stateMachine/stateMachine.go (100%) diff --git a/.gitignore b/.gitignore index 0471d0c..bd124b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /base.db .env -/.idea \ No newline at end of file +/.idea +config/local.yaml \ No newline at end of file diff --git a/Makefile b/Makefile index 2e516d8..71d6054 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,7 @@ gen: --go-grpc_out=. \ --go-grpc_opt=paths=source_relative \ ./protos/*.proto + +.PHONY: dev +dev: + go run ./cmd/algobot/main.go -config=./config/local.yaml \ No newline at end of file diff --git a/cmd/algobot/main.go b/cmd/algobot/main.go new file mode 100644 index 0000000..6acba0e --- /dev/null +++ b/cmd/algobot/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "algobot/internal/config" + "fmt" +) + +func main() { + cfg := config.MustLoad() + fmt.Printf("%#v\n", cfg) + + // TODO : create application + + // TODO : start bot app + // TODO : start message scheduler app + + // graceful shutdown + + // TODO : add graceful shutdown +} diff --git a/cmd/main.go b/cmd/main.go index ca788ef..2441985 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,13 @@ package main import ( + "algobot/internal_old/clients" + "algobot/internal_old/contextHandlers" + "algobot/internal_old/domain" + "algobot/internal_old/middleware" + "algobot/internal_old/schedulers" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "database/sql" "embed" "github.com/joho/godotenv" @@ -10,13 +17,6 @@ import ( middleware2 "gopkg.in/telebot.v4/middleware" "log" "os" - "tgbot/internal/clients" - "tgbot/internal/contextHandlers" - "tgbot/internal/domain" - "tgbot/internal/middleware" - "tgbot/internal/schedulers" - "tgbot/internal/service" - "tgbot/internal/stateMachine" "time" ) diff --git a/config/dev.yaml b/config/dev.yaml new file mode 100644 index 0000000..1d950d0 --- /dev/null +++ b/config/dev.yaml @@ -0,0 +1,8 @@ +env: local # or prod +storage_path: "./storage/base.db" +# telegram token set in env variables - TELEGRAM_TOKEN +migrations_path: "./migrations" +grpc: + host: "localhost" + port: 50051 + timeout: 999s diff --git a/go.mod b/go.mod index 4a82b5c..ffaf0e3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module tgbot +module algobot go 1.23.3 @@ -13,7 +13,9 @@ require ( ) require ( + github.com/BurntSushi/toml v1.5.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect @@ -21,4 +23,6 @@ require ( golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 01ad168..15e4d48 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -258,6 +261,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -944,6 +949,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3da393d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,43 @@ +package config + +import ( + "flag" + "github.com/ilyakaznacheev/cleanenv" + "os" +) + +type Config struct { + Env string `yaml:"env" env-default:"prod"` + TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` + MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` + GRPC GRPC `yaml:"grpc"` +} + +type GRPC struct { + Host string `yaml:"host" env-default:"localhost"` + Port string `yaml:"port" env-default:"50051"` + Timeout string `yaml:"timeout" env-default:"600s"` +} + +func MustLoad() *Config { + var configPath string + flag.StringVar(&configPath, "config", "", "path to config file") + flag.Parse() + + if configPath == "" { + configPath = os.Getenv("CONFIG_PATH") + } + if configPath == "" { + panic("config file path is empty") + } + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + panic("config file does not exist : " + err.Error()) + } + var cfg Config + if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { + panic("can't read config : " + err.Error()) + } + + return &cfg +} diff --git a/internal/clients/backoffice.go b/internal_old/clients/backoffice.go similarity index 99% rename from internal/clients/backoffice.go rename to internal_old/clients/backoffice.go index 45f9521..f126dd4 100644 --- a/internal/clients/backoffice.go +++ b/internal_old/clients/backoffice.go @@ -1,6 +1,7 @@ package clients import ( + appError "algobot/internal_old/error" "encoding/json" "errors" "fmt" @@ -10,7 +11,6 @@ import ( "net/url" "strconv" "strings" - appError "tgbot/internal/error" "time" ) diff --git a/internal/clients/webClient.go b/internal_old/clients/webClient.go similarity index 100% rename from internal/clients/webClient.go rename to internal_old/clients/webClient.go diff --git a/internal/config/keyboards.go b/internal_old/config/keyboards.go similarity index 100% rename from internal/config/keyboards.go rename to internal_old/config/keyboards.go diff --git a/internal/config/texts.go b/internal_old/config/texts.go similarity index 100% rename from internal/config/texts.go rename to internal_old/config/texts.go diff --git a/internal/contextHandlers/callbackHandlers/changeNotification.go b/internal_old/contextHandlers/callbackHandlers/changeNotification.go similarity index 84% rename from internal/contextHandlers/callbackHandlers/changeNotification.go rename to internal_old/contextHandlers/callbackHandlers/changeNotification.go index 0a142e4..f4039d7 100644 --- a/internal/contextHandlers/callbackHandlers/changeNotification.go +++ b/internal_old/contextHandlers/callbackHandlers/changeNotification.go @@ -1,11 +1,11 @@ package callbackHandlers import ( + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/contextHandlers/textHandlers/defaultState" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "gopkg.in/telebot.v4" - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/contextHandlers/textHandlers/defaultState" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type ChangeNotification struct { diff --git a/internal/contextHandlers/callbackHandlers/closeLesson.go b/internal_old/contextHandlers/callbackHandlers/closeLesson.go similarity index 90% rename from internal/contextHandlers/callbackHandlers/closeLesson.go rename to internal_old/contextHandlers/callbackHandlers/closeLesson.go index 21a70f2..305dcdb 100644 --- a/internal/contextHandlers/callbackHandlers/closeLesson.go +++ b/internal_old/contextHandlers/callbackHandlers/closeLesson.go @@ -1,14 +1,14 @@ package callbackHandlers import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "fmt" "gopkg.in/telebot.v4" "strconv" "strings" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type CloseLesson struct { diff --git a/internal/contextHandlers/callbackHandlers/getCredentials.go b/internal_old/contextHandlers/callbackHandlers/getCredentials.go similarity index 94% rename from internal/contextHandlers/callbackHandlers/getCredentials.go rename to internal_old/contextHandlers/callbackHandlers/getCredentials.go index cf909fa..d86bba6 100644 --- a/internal/contextHandlers/callbackHandlers/getCredentials.go +++ b/internal_old/contextHandlers/callbackHandlers/getCredentials.go @@ -1,12 +1,12 @@ package callbackHandlers import ( + "algobot/internal_old/helpers" + "algobot/internal_old/service" "fmt" "gopkg.in/telebot.v4" "strconv" "strings" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type GetCredentials struct { diff --git a/internal/contextHandlers/callbackHandlers/openLesson.go b/internal_old/contextHandlers/callbackHandlers/openLesson.go similarity index 90% rename from internal/contextHandlers/callbackHandlers/openLesson.go rename to internal_old/contextHandlers/callbackHandlers/openLesson.go index d366db1..8a94f40 100644 --- a/internal/contextHandlers/callbackHandlers/openLesson.go +++ b/internal_old/contextHandlers/callbackHandlers/openLesson.go @@ -1,14 +1,14 @@ package callbackHandlers import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "fmt" "gopkg.in/telebot.v4" "strconv" "strings" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type OpenLesson struct { diff --git a/internal/contextHandlers/callbackHandlers/refreshGroups.go b/internal_old/contextHandlers/callbackHandlers/refreshGroups.go similarity index 86% rename from internal/contextHandlers/callbackHandlers/refreshGroups.go rename to internal_old/contextHandlers/callbackHandlers/refreshGroups.go index 4eea2b8..726097d 100644 --- a/internal/contextHandlers/callbackHandlers/refreshGroups.go +++ b/internal_old/contextHandlers/callbackHandlers/refreshGroups.go @@ -1,12 +1,12 @@ package callbackHandlers import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "errors" "gopkg.in/telebot.v4" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type RefreshGroups struct { diff --git a/internal/contextHandlers/callbackHandlers/setCookie.go b/internal_old/contextHandlers/callbackHandlers/setCookie.go similarity index 85% rename from internal/contextHandlers/callbackHandlers/setCookie.go rename to internal_old/contextHandlers/callbackHandlers/setCookie.go index b2a01b8..9b87350 100644 --- a/internal/contextHandlers/callbackHandlers/setCookie.go +++ b/internal_old/contextHandlers/callbackHandlers/setCookie.go @@ -1,10 +1,10 @@ package callbackHandlers import ( + "algobot/internal_old/config" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/service" - "tgbot/internal/stateMachine" ) type SetCookie struct { diff --git a/internal/contextHandlers/defaultHandler/handler.go b/internal_old/contextHandlers/defaultHandler/handler.go similarity index 100% rename from internal/contextHandlers/defaultHandler/handler.go rename to internal_old/contextHandlers/defaultHandler/handler.go diff --git a/internal/contextHandlers/handlersHolders/DefaultCBHolder.go b/internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go similarity index 81% rename from internal/contextHandlers/handlersHolders/DefaultCBHolder.go rename to internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go index f973b8b..ad44719 100644 --- a/internal/contextHandlers/handlersHolders/DefaultCBHolder.go +++ b/internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go @@ -1,10 +1,10 @@ package handlersHolders import ( - "tgbot/internal/contextHandlers/callbackHandlers" - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/service" - "tgbot/internal/stateMachine" + "algobot/internal_old/contextHandlers/callbackHandlers" + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" ) type DefaultCBHolder struct { diff --git a/internal/contextHandlers/handlersHolders/DefaultHolder.go b/internal_old/contextHandlers/handlersHolders/DefaultHolder.go similarity index 80% rename from internal/contextHandlers/handlersHolders/DefaultHolder.go rename to internal_old/contextHandlers/handlersHolders/DefaultHolder.go index b53dd13..f0920c0 100644 --- a/internal/contextHandlers/handlersHolders/DefaultHolder.go +++ b/internal_old/contextHandlers/handlersHolders/DefaultHolder.go @@ -1,10 +1,10 @@ package handlersHolders import ( - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/contextHandlers/textHandlers/defaultState" - "tgbot/internal/service" - "tgbot/internal/stateMachine" + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/contextHandlers/textHandlers/defaultState" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" ) type DefaultHolders struct { diff --git a/internal/contextHandlers/handlersHolders/SendingCookie.go b/internal_old/contextHandlers/handlersHolders/SendingCookie.go similarity index 75% rename from internal/contextHandlers/handlersHolders/SendingCookie.go rename to internal_old/contextHandlers/handlersHolders/SendingCookie.go index f80811a..f063d8f 100644 --- a/internal/contextHandlers/handlersHolders/SendingCookie.go +++ b/internal_old/contextHandlers/handlersHolders/SendingCookie.go @@ -1,10 +1,10 @@ package handlersHolders import ( - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/contextHandlers/textHandlers/sendingCookieState" - "tgbot/internal/service" - "tgbot/internal/stateMachine" + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/contextHandlers/textHandlers/sendingCookieState" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" ) type SendingCookie struct { diff --git a/internal/contextHandlers/handlersHolders/chattingAI.go b/internal_old/contextHandlers/handlersHolders/chattingAI.go similarity index 77% rename from internal/contextHandlers/handlersHolders/chattingAI.go rename to internal_old/contextHandlers/handlersHolders/chattingAI.go index b8f083c..851b19f 100644 --- a/internal/contextHandlers/handlersHolders/chattingAI.go +++ b/internal_old/contextHandlers/handlersHolders/chattingAI.go @@ -1,10 +1,10 @@ package handlersHolders import ( - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/contextHandlers/textHandlers/chattingAi" - "tgbot/internal/service" - "tgbot/internal/stateMachine" + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/contextHandlers/textHandlers/chattingAi" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" ) type ChattingAi struct { diff --git a/internal/contextHandlers/handlersHolders/holder.go b/internal_old/contextHandlers/handlersHolders/holder.go similarity index 62% rename from internal/contextHandlers/handlersHolders/holder.go rename to internal_old/contextHandlers/handlersHolders/holder.go index d70fd54..f9b0481 100644 --- a/internal/contextHandlers/handlersHolders/holder.go +++ b/internal_old/contextHandlers/handlersHolders/holder.go @@ -1,8 +1,8 @@ package handlersHolders import ( - "tgbot/internal/contextHandlers/defaultHandler" - "tgbot/internal/stateMachine" + "algobot/internal_old/contextHandlers/defaultHandler" + "algobot/internal_old/stateMachine" ) type HandlersHolder interface { diff --git a/internal/contextHandlers/onCallback.go b/internal_old/contextHandlers/onCallback.go similarity index 88% rename from internal/contextHandlers/onCallback.go rename to internal_old/contextHandlers/onCallback.go index 32533e8..ed5352c 100644 --- a/internal/contextHandlers/onCallback.go +++ b/internal_old/contextHandlers/onCallback.go @@ -1,15 +1,15 @@ package contextHandlers import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers/handlersHolders" + "algobot/internal_old/helpers" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "errors" "fmt" "gopkg.in/telebot.v4" "strings" - "tgbot/internal/config" - "tgbot/internal/contextHandlers/handlersHolders" - "tgbot/internal/helpers" - "tgbot/internal/service" - "tgbot/internal/stateMachine" ) type OnCallback struct { diff --git a/internal/contextHandlers/onText.go b/internal_old/contextHandlers/onText.go similarity index 88% rename from internal/contextHandlers/onText.go rename to internal_old/contextHandlers/onText.go index e11e0b7..4a0c545 100644 --- a/internal/contextHandlers/onText.go +++ b/internal_old/contextHandlers/onText.go @@ -1,14 +1,14 @@ package contextHandlers import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers/handlersHolders" + "algobot/internal_old/helpers" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "errors" "fmt" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/contextHandlers/handlersHolders" - "tgbot/internal/helpers" - "tgbot/internal/service" - "tgbot/internal/stateMachine" ) type OnText struct { diff --git a/internal/contextHandlers/textHandlers/chattingAi/AnyMessage.go b/internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go similarity index 86% rename from internal/contextHandlers/textHandlers/chattingAi/AnyMessage.go rename to internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go index 766660a..5ee97c9 100644 --- a/internal/contextHandlers/textHandlers/chattingAi/AnyMessage.go +++ b/internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go @@ -1,12 +1,12 @@ package chattingAi import ( + "algobot/internal_old/config" + "algobot/internal_old/helpers" + "algobot/internal_old/schedulers" + "algobot/internal_old/service" "gopkg.in/telebot.v4" "strconv" - "tgbot/internal/config" - "tgbot/internal/helpers" - "tgbot/internal/schedulers" - "tgbot/internal/service" ) type AnyMessage struct { diff --git a/internal/contextHandlers/textHandlers/chattingAi/ClearHistory.go b/internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go similarity index 86% rename from internal/contextHandlers/textHandlers/chattingAi/ClearHistory.go rename to internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go index f349e46..7dd7724 100644 --- a/internal/contextHandlers/textHandlers/chattingAi/ClearHistory.go +++ b/internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go @@ -1,10 +1,10 @@ package chattingAi import ( + "algobot/internal_old/config" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type ClearHistory struct { diff --git a/internal/contextHandlers/textHandlers/chattingAi/backAction.go b/internal_old/contextHandlers/textHandlers/chattingAi/backAction.go similarity index 88% rename from internal/contextHandlers/textHandlers/chattingAi/backAction.go rename to internal_old/contextHandlers/textHandlers/chattingAi/backAction.go index 28276ad..765d1c0 100644 --- a/internal/contextHandlers/textHandlers/chattingAi/backAction.go +++ b/internal_old/contextHandlers/textHandlers/chattingAi/backAction.go @@ -1,9 +1,9 @@ package chattingAi import ( + "algobot/internal_old/config" + "algobot/internal_old/stateMachine" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/stateMachine" ) type BackAction struct { diff --git a/internal/contextHandlers/textHandlers/defaultState/absentKids.go b/internal_old/contextHandlers/textHandlers/defaultState/absentKids.go similarity index 93% rename from internal/contextHandlers/textHandlers/defaultState/absentKids.go rename to internal_old/contextHandlers/textHandlers/defaultState/absentKids.go index e205dbb..5d224f0 100644 --- a/internal/contextHandlers/textHandlers/defaultState/absentKids.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/absentKids.go @@ -1,13 +1,13 @@ package defaultState import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "errors" "gopkg.in/telebot.v4" "strings" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/service" "time" ) diff --git a/internal/contextHandlers/textHandlers/defaultState/aiChat.go b/internal_old/contextHandlers/textHandlers/defaultState/aiChat.go similarity index 86% rename from internal/contextHandlers/textHandlers/defaultState/aiChat.go rename to internal_old/contextHandlers/textHandlers/defaultState/aiChat.go index 5b650ec..86a232d 100644 --- a/internal/contextHandlers/textHandlers/defaultState/aiChat.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/aiChat.go @@ -1,10 +1,10 @@ package defaultState import ( + "algobot/internal_old/config" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/service" - "tgbot/internal/stateMachine" ) type AIChat struct { diff --git a/internal/contextHandlers/textHandlers/defaultState/missingKids.go b/internal_old/contextHandlers/textHandlers/defaultState/missingKids.go similarity index 94% rename from internal/contextHandlers/textHandlers/defaultState/missingKids.go rename to internal_old/contextHandlers/textHandlers/defaultState/missingKids.go index 4190e43..d0c1423 100644 --- a/internal/contextHandlers/textHandlers/defaultState/missingKids.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/missingKids.go @@ -1,15 +1,15 @@ package defaultState import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/models" + "algobot/internal_old/service" "errors" "fmt" "gopkg.in/telebot.v4" "strings" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/models" - "tgbot/internal/service" ) type MissingKids struct { diff --git a/internal/contextHandlers/textHandlers/defaultState/myGroups.go b/internal_old/contextHandlers/textHandlers/defaultState/myGroups.go similarity index 90% rename from internal/contextHandlers/textHandlers/defaultState/myGroups.go rename to internal_old/contextHandlers/textHandlers/defaultState/myGroups.go index 4bbbe40..ead6853 100644 --- a/internal/contextHandlers/textHandlers/defaultState/myGroups.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/myGroups.go @@ -1,18 +1,18 @@ package defaultState import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/models" + "algobot/internal_old/serdes" + "algobot/internal_old/service" "errors" "fmt" "gopkg.in/telebot.v4" "os" "strconv" "strings" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/models" - "tgbot/internal/serdes" - "tgbot/internal/service" "time" ) diff --git a/internal/contextHandlers/textHandlers/defaultState/settings.go b/internal_old/contextHandlers/textHandlers/defaultState/settings.go similarity index 93% rename from internal/contextHandlers/textHandlers/defaultState/settings.go rename to internal_old/contextHandlers/textHandlers/defaultState/settings.go index 92e3db8..93b8f75 100644 --- a/internal/contextHandlers/textHandlers/defaultState/settings.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/settings.go @@ -1,11 +1,11 @@ package defaultState import ( + "algobot/internal_old/config" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "gopkg.in/telebot.v4" "strings" - "tgbot/internal/config" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type Settings struct { diff --git a/internal/contextHandlers/textHandlers/defaultState/start.go b/internal_old/contextHandlers/textHandlers/defaultState/start.go similarity index 91% rename from internal/contextHandlers/textHandlers/defaultState/start.go rename to internal_old/contextHandlers/textHandlers/defaultState/start.go index 64d9065..ff77c1f 100644 --- a/internal/contextHandlers/textHandlers/defaultState/start.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/start.go @@ -1,8 +1,8 @@ package defaultState import ( + "algobot/internal_old/config" "gopkg.in/telebot.v4" - "tgbot/internal/config" ) type Start struct { diff --git a/internal/contextHandlers/textHandlers/defaultState/startWithPayload.go b/internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go similarity index 97% rename from internal/contextHandlers/textHandlers/defaultState/startWithPayload.go rename to internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go index d467413..72acf4c 100644 --- a/internal/contextHandlers/textHandlers/defaultState/startWithPayload.go +++ b/internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go @@ -1,16 +1,16 @@ package defaultState import ( + "algobot/internal_old/helpers" + "algobot/internal_old/models" + "algobot/internal_old/serdes" + "algobot/internal_old/service" "fmt" "gopkg.in/telebot.v4" "os" "regexp" "strconv" "strings" - "tgbot/internal/helpers" - "tgbot/internal/models" - "tgbot/internal/serdes" - "tgbot/internal/service" ) var statuses = map[int]string{ diff --git a/internal/contextHandlers/textHandlers/sendingCookieState/rejectAction.go b/internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go similarity index 89% rename from internal/contextHandlers/textHandlers/sendingCookieState/rejectAction.go rename to internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go index 9f1fedd..4e7a385 100644 --- a/internal/contextHandlers/textHandlers/sendingCookieState/rejectAction.go +++ b/internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go @@ -1,9 +1,9 @@ package sendingCookieState import ( + "algobot/internal_old/config" + "algobot/internal_old/stateMachine" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/stateMachine" ) type RejectAction struct { diff --git a/internal/contextHandlers/textHandlers/sendingCookieState/sendCookie.go b/internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go similarity index 86% rename from internal/contextHandlers/textHandlers/sendingCookieState/sendCookie.go rename to internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go index 0d17782..d6c5f00 100644 --- a/internal/contextHandlers/textHandlers/sendingCookieState/sendCookie.go +++ b/internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go @@ -1,11 +1,11 @@ package sendingCookieState import ( + "algobot/internal_old/config" + "algobot/internal_old/helpers" + "algobot/internal_old/service" + "algobot/internal_old/stateMachine" "gopkg.in/telebot.v4" - "tgbot/internal/config" - "tgbot/internal/helpers" - "tgbot/internal/service" - "tgbot/internal/stateMachine" ) type SendingCookieAction struct { diff --git a/internal/domain/domain.go b/internal_old/domain/domain.go similarity index 100% rename from internal/domain/domain.go rename to internal_old/domain/domain.go diff --git a/internal/domain/sqlite3.go b/internal_old/domain/sqlite3.go similarity index 99% rename from internal/domain/sqlite3.go rename to internal_old/domain/sqlite3.go index 4be830e..49fa786 100644 --- a/internal/domain/sqlite3.go +++ b/internal_old/domain/sqlite3.go @@ -1,12 +1,12 @@ package domain import ( + appError "algobot/internal_old/error" "database/sql" "errors" "fmt" "io/fs" "log" - appError "tgbot/internal/error" "time" ) diff --git a/internal/error/error.go b/internal_old/error/error.go similarity index 100% rename from internal/error/error.go rename to internal_old/error/error.go diff --git a/internal/helpers/group.go b/internal_old/helpers/group.go similarity index 96% rename from internal/helpers/group.go rename to internal_old/helpers/group.go index 7ab2b56..fe0c9eb 100644 --- a/internal/helpers/group.go +++ b/internal_old/helpers/group.go @@ -1,9 +1,9 @@ package helpers import ( + appError "algobot/internal_old/error" + "algobot/internal_old/models" "sort" - appError "tgbot/internal/error" - "tgbot/internal/models" "time" ) diff --git a/internal/helpers/group_test.go b/internal_old/helpers/group_test.go similarity index 98% rename from internal/helpers/group_test.go rename to internal_old/helpers/group_test.go index 0697a31..bc7d37c 100644 --- a/internal/helpers/group_test.go +++ b/internal_old/helpers/group_test.go @@ -1,9 +1,9 @@ package helpers import ( + "algobot/internal_old/models" "reflect" "testing" - "tgbot/internal/models" "time" ) diff --git a/internal/helpers/logError.go b/internal_old/helpers/logError.go similarity index 100% rename from internal/helpers/logError.go rename to internal_old/helpers/logError.go diff --git a/internal/middleware/logger.go b/internal_old/middleware/logger.go similarity index 100% rename from internal/middleware/logger.go rename to internal_old/middleware/logger.go diff --git a/internal/middleware/register.go b/internal_old/middleware/register.go similarity index 86% rename from internal/middleware/register.go rename to internal_old/middleware/register.go index be49274..bbc8509 100644 --- a/internal/middleware/register.go +++ b/internal_old/middleware/register.go @@ -1,12 +1,12 @@ package middleware import ( + "algobot/internal_old/config" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/service" "errors" "gopkg.in/telebot.v4" - "tgbot/internal/config" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/service" ) type Register struct { diff --git a/internal/models/models.go b/internal_old/models/models.go similarity index 95% rename from internal/models/models.go rename to internal_old/models/models.go index 49fc349..1b7418a 100644 --- a/internal/models/models.go +++ b/internal_old/models/models.go @@ -1,8 +1,8 @@ package models import ( - "tgbot/internal/clients" - "tgbot/internal/domain" + "algobot/internal_old/clients" + "algobot/internal_old/domain" "time" ) diff --git a/internal/models/startPayload.go b/internal_old/models/startPayload.go similarity index 100% rename from internal/models/startPayload.go rename to internal_old/models/startPayload.go diff --git a/internal/schedulers/message.go b/internal_old/schedulers/message.go similarity index 95% rename from internal/schedulers/message.go rename to internal_old/schedulers/message.go index 0e05928..ec663e4 100644 --- a/internal/schedulers/message.go +++ b/internal_old/schedulers/message.go @@ -1,13 +1,13 @@ package schedulers import ( + "algobot/internal_old/models" + "algobot/internal_old/service" "fmt" "gopkg.in/telebot.v4" "log" "strconv" "strings" - "tgbot/internal/models" - "tgbot/internal/service" ) type Message struct { diff --git a/internal/serdes/simple.go b/internal_old/serdes/simple.go similarity index 94% rename from internal/serdes/simple.go rename to internal_old/serdes/simple.go index c73dc93..f430e6d 100644 --- a/internal/serdes/simple.go +++ b/internal_old/serdes/simple.go @@ -1,10 +1,10 @@ package serdes import ( + "algobot/internal_old/models" "fmt" "github.com/jxskiss/base62" "strings" - "tgbot/internal/models" ) func Serialize(m models.StartPayload) string { diff --git a/internal/service/AIService.go b/internal_old/service/AIService.go similarity index 98% rename from internal/service/AIService.go rename to internal_old/service/AIService.go index fe157c4..4d3bdf7 100644 --- a/internal/service/AIService.go +++ b/internal_old/service/AIService.go @@ -1,12 +1,12 @@ package service import ( + pkg "algobot/protos" "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "log" - pkg "tgbot/protos" ) type AIService interface { diff --git a/internal/service/DefaultService.go b/internal_old/service/DefaultService.go similarity index 98% rename from internal/service/DefaultService.go rename to internal_old/service/DefaultService.go index be2fe17..fc6a66c 100644 --- a/internal/service/DefaultService.go +++ b/internal_old/service/DefaultService.go @@ -1,17 +1,17 @@ package service import ( + "algobot/internal_old/clients" + "algobot/internal_old/domain" + appError "algobot/internal_old/error" + "algobot/internal_old/helpers" + "algobot/internal_old/models" "errors" "fmt" "log" "regexp" "strconv" "strings" - "tgbot/internal/clients" - "tgbot/internal/domain" - appError "tgbot/internal/error" - "tgbot/internal/helpers" - "tgbot/internal/models" "time" ) diff --git a/internal/service/service.go b/internal_old/service/service.go similarity index 97% rename from internal/service/service.go rename to internal_old/service/service.go index 6e49af1..003538e 100644 --- a/internal/service/service.go +++ b/internal_old/service/service.go @@ -1,7 +1,7 @@ package service import ( - "tgbot/internal/models" + "algobot/internal_old/models" "time" ) diff --git a/internal/stateMachine/memory.go b/internal_old/stateMachine/memory.go similarity index 100% rename from internal/stateMachine/memory.go rename to internal_old/stateMachine/memory.go diff --git a/internal/stateMachine/stateMachine.go b/internal_old/stateMachine/stateMachine.go similarity index 100% rename from internal/stateMachine/stateMachine.go rename to internal_old/stateMachine/stateMachine.go diff --git a/tests/clients/backoffice_test.go b/tests/clients/backoffice_test.go index ab54cba..f08b3a3 100644 --- a/tests/clients/backoffice_test.go +++ b/tests/clients/backoffice_test.go @@ -1,6 +1,7 @@ package test import ( + "algobot/internal_old/clients" "context" "encoding/json" "errors" @@ -12,7 +13,6 @@ import ( "os" "reflect" "testing" - "tgbot/internal/clients" "time" ) diff --git a/tests/domain/domain_test.go b/tests/domain/domain_test.go index 9700a91..a1f3609 100644 --- a/tests/domain/domain_test.go +++ b/tests/domain/domain_test.go @@ -1,6 +1,8 @@ package domain import ( + "algobot/internal_old/domain" + appError "algobot/internal_old/error" "database/sql" "errors" "fmt" @@ -10,8 +12,6 @@ import ( "os" "reflect" "testing" - "tgbot/internal/domain" - appError "tgbot/internal/error" "time" ) diff --git a/tests/handlers/chattingAI/chattingAI_test.go b/tests/handlers/chattingAI/chattingAI_test.go index ef430fe..d784482 100644 --- a/tests/handlers/chattingAI/chattingAI_test.go +++ b/tests/handlers/chattingAI/chattingAI_test.go @@ -1,16 +1,16 @@ package test import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers" + "algobot/internal_old/stateMachine" + "algobot/tests/mocks" "errors" "github.com/golang/mock/gomock" "gopkg.in/telebot.v4" "reflect" "strings" "testing" - "tgbot/internal/config" - "tgbot/internal/contextHandlers" - "tgbot/internal/stateMachine" - "tgbot/tests/mocks" ) func TestSending(t *testing.T) { diff --git a/tests/handlers/defaultState/CallbackHandler_test.go b/tests/handlers/defaultState/CallbackHandler_test.go index fda4338..5f50959 100644 --- a/tests/handlers/defaultState/CallbackHandler_test.go +++ b/tests/handlers/defaultState/CallbackHandler_test.go @@ -1,12 +1,12 @@ package test import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers" + "algobot/internal_old/stateMachine" + "algobot/tests/mocks" "fmt" "testing" - "tgbot/internal/config" - "tgbot/internal/contextHandlers" - "tgbot/internal/stateMachine" - "tgbot/tests/mocks" ) func TestCallback(t *testing.T) { diff --git a/tests/handlers/defaultState/DefaultHandler_test.go b/tests/handlers/defaultState/DefaultHandler_test.go index a4bc3fa..dcdeb21 100644 --- a/tests/handlers/defaultState/DefaultHandler_test.go +++ b/tests/handlers/defaultState/DefaultHandler_test.go @@ -1,6 +1,14 @@ package test import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers" + "algobot/internal_old/contextHandlers/textHandlers/defaultState" + appError "algobot/internal_old/error" + "algobot/internal_old/models" + "algobot/internal_old/serdes" + "algobot/internal_old/stateMachine" + "algobot/tests/mocks" "errors" "fmt" "github.com/golang/mock/gomock" @@ -9,14 +17,6 @@ import ( "reflect" "strings" "testing" - "tgbot/internal/config" - "tgbot/internal/contextHandlers" - "tgbot/internal/contextHandlers/textHandlers/defaultState" - appError "tgbot/internal/error" - "tgbot/internal/models" - "tgbot/internal/serdes" - "tgbot/internal/stateMachine" - "tgbot/tests/mocks" "time" ) diff --git a/tests/handlers/sendCookieState/sendCookieHandler_test.go b/tests/handlers/sendCookieState/sendCookieHandler_test.go index 17abf0a..da8ec11 100644 --- a/tests/handlers/sendCookieState/sendCookieHandler_test.go +++ b/tests/handlers/sendCookieState/sendCookieHandler_test.go @@ -1,14 +1,14 @@ package test import ( + "algobot/internal_old/config" + "algobot/internal_old/contextHandlers" + "algobot/internal_old/stateMachine" + "algobot/tests/mocks" "github.com/golang/mock/gomock" "gopkg.in/telebot.v4" "reflect" "testing" - "tgbot/internal/config" - "tgbot/internal/contextHandlers" - "tgbot/internal/stateMachine" - "tgbot/tests/mocks" ) func TestSending(t *testing.T) { diff --git a/tests/mocks/AIService_mock.go b/tests/mocks/AIService_mock.go index 0b4074a..81a7b0b 100644 --- a/tests/mocks/AIService_mock.go +++ b/tests/mocks/AIService_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: tgbot/internal/service (interfaces: AIService) +// Source: tgbot/internal_old/service (interfaces: AIService) // Package mocks is a generated GoMock package. package mocks diff --git a/tests/mocks/MockStateMachine.go b/tests/mocks/MockStateMachine.go index f6dd3d0..864756d 100644 --- a/tests/mocks/MockStateMachine.go +++ b/tests/mocks/MockStateMachine.go @@ -1,8 +1,8 @@ package mocks import ( + "algobot/internal_old/stateMachine" "strconv" - "tgbot/internal/stateMachine" ) type MockStateMachine struct { diff --git a/tests/mocks/mockDomain.go b/tests/mocks/mockDomain.go index 87da94a..27ae17e 100644 --- a/tests/mocks/mockDomain.go +++ b/tests/mocks/mockDomain.go @@ -1,7 +1,7 @@ package mocks import ( - "tgbot/internal/domain" + "algobot/internal_old/domain" "time" ) diff --git a/tests/mocks/mockWebClient.go b/tests/mocks/mockWebClient.go index cb7af96..8b6aede 100644 --- a/tests/mocks/mockWebClient.go +++ b/tests/mocks/mockWebClient.go @@ -1,7 +1,7 @@ package mocks import ( - "tgbot/internal/clients" + "algobot/internal_old/clients" "time" ) diff --git a/tests/mocks/newMockService.go b/tests/mocks/newMockService.go index 96590ba..edf5528 100644 --- a/tests/mocks/newMockService.go +++ b/tests/mocks/newMockService.go @@ -1,10 +1,10 @@ package mocks import ( + "algobot/internal_old/models" "errors" "fmt" "strconv" - "tgbot/internal/models" "time" ) diff --git a/tests/scheduler/message_test.go b/tests/scheduler/message_test.go index b75129b..da7d837 100644 --- a/tests/scheduler/message_test.go +++ b/tests/scheduler/message_test.go @@ -1,9 +1,9 @@ package scheduler_test import ( + "algobot/internal_old/schedulers" + "algobot/tests/mocks" "testing" - "tgbot/internal/schedulers" - "tgbot/tests/mocks" ) func TestScheduler(t *testing.T) { diff --git a/tests/services/service_test.go b/tests/services/service_test.go index 954dc82..8df2a4e 100644 --- a/tests/services/service_test.go +++ b/tests/services/service_test.go @@ -1,13 +1,13 @@ package services import ( + "algobot/internal_old/domain" + appError "algobot/internal_old/error" + "algobot/internal_old/models" + "algobot/internal_old/service" + "algobot/tests/mocks" "reflect" "testing" - "tgbot/internal/domain" - appError "tgbot/internal/error" - "tgbot/internal/models" - "tgbot/internal/service" - "tgbot/tests/mocks" "time" ) diff --git a/tests/stateMachine_test.go b/tests/stateMachine_test.go index 55528cc..e40bb21 100644 --- a/tests/stateMachine_test.go +++ b/tests/stateMachine_test.go @@ -1,8 +1,8 @@ package tests_test import ( + stateMachine2 "algobot/internal_old/stateMachine" "testing" - stateMachine2 "tgbot/internal/stateMachine" ) func TestState(t *testing.T) { From c067570fc0b479cde24b72b63db113b000374458 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 28 Mar 2025 12:39:52 +0300 Subject: [PATCH 02/44] add tracing + logger middleware --- cmd/algobot/main.go | 38 ++- go.mod | 5 +- go.sum | 4 + internal/app/app.go | 20 ++ internal/app/telegram/app.go | 57 ++++ .../logger/handlers/slogpretty/slogpretty.go | 256 ++++++++++++++++++ internal/lib/logger/sl/sl.go | 10 + internal/telegram/middleware/logger/logger.go | 36 +++ internal/telegram/middleware/trace/trace.go | 32 +++ test_v2/mocks/mockgen.go | 1 + 10 files changed, 453 insertions(+), 6 deletions(-) create mode 100644 internal/app/app.go create mode 100644 internal/app/telegram/app.go create mode 100644 internal/lib/logger/handlers/slogpretty/slogpretty.go create mode 100644 internal/lib/logger/sl/sl.go create mode 100644 internal/telegram/middleware/logger/logger.go create mode 100644 internal/telegram/middleware/trace/trace.go create mode 100644 test_v2/mocks/mockgen.go diff --git a/cmd/algobot/main.go b/cmd/algobot/main.go index 6acba0e..cff4896 100644 --- a/cmd/algobot/main.go +++ b/cmd/algobot/main.go @@ -1,20 +1,50 @@ package main import ( + "algobot/internal/app" "algobot/internal/config" - "fmt" + "algobot/internal/lib/logger/handlers/slogpretty" + "log/slog" + "os" + "os/signal" + "syscall" +) + +const ( + envProd string = "prod" + envLocal string = "local" ) func main() { cfg := config.MustLoad() - fmt.Printf("%#v\n", cfg) + log := setupLogger(cfg.Env) + log.Info("starting application") - // TODO : create application + application := app.New(log, cfg) + go application.TelegramBot.Run() + log.Info("started telegram bot") // TODO : start bot app // TODO : start message scheduler app // graceful shutdown + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGTERM) + <-ch + log.Info("shutting down application") + application.TelegramBot.Stop() + log.Info("application gracefully stopped") +} + +func setupLogger(env string) *slog.Logger { + var log *slog.Logger + + switch env { + case envLocal: + log = slog.New(slogpretty.NewHandler(&slog.HandlerOptions{Level: slog.LevelDebug})) + case envProd: + log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + } - // TODO : add graceful shutdown + return log } diff --git a/go.mod b/go.mod index ffaf0e3..47ca611 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.23.3 require ( github.com/PuerkitoBio/goquery v1.10.1 github.com/golang/mock v1.6.0 + github.com/google/uuid v1.6.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/joho/godotenv v1.5.1 github.com/jxskiss/base62 v1.1.0 github.com/ncruces/go-sqlite3 v0.22.0 google.golang.org/grpc v1.71.0 @@ -15,8 +18,6 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect - github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect golang.org/x/net v0.34.0 // indirect diff --git a/go.sum b/go.sum index 15e4d48..a20df29 100644 --- a/go.sum +++ b/go.sum @@ -285,9 +285,11 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -356,6 +358,7 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= @@ -925,6 +928,7 @@ google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..92b168a --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,20 @@ +package app + +import ( + "algobot/internal/app/telegram" + "algobot/internal/config" + "log/slog" +) + +type App struct { + log *slog.Logger + cfg *config.Config + TelegramBot *telegram.App +} + +func New(log *slog.Logger, cfg *config.Config) *App { + + botApplicaton := telegram.New(log, cfg.TelegramToken) + + return &App{log: log, cfg: cfg, TelegramBot: botApplicaton} +} diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go new file mode 100644 index 0000000..c007973 --- /dev/null +++ b/internal/app/telegram/app.go @@ -0,0 +1,57 @@ +package telegram + +import ( + "algobot/internal/lib/logger/sl" + "algobot/internal/telegram/middleware/logger" + "algobot/internal/telegram/middleware/trace" + tele "gopkg.in/telebot.v4" + "gopkg.in/telebot.v4/middleware" + "log/slog" + "os" + "time" +) + +type App struct { + log *slog.Logger + bot *tele.Bot +} + +func New(log *slog.Logger, token string) *App { + const op = "telegram.New" + + nlog := log.With( + slog.String("op", op), + ) + + pref := tele.Settings{ + Token: token, + Poller: &tele.LongPoller{ + Timeout: 10 * time.Second, + }, + } + b, err := tele.NewBot(pref) + if err != nil { + nlog.Warn("error by creating telegram bot: ", sl.Err(err)) + os.Exit(1) + } + + // initialize routes + b.Use(trace.New(log)) + b.Use(middleware.AutoRespond()) + b.Use(middleware.Recover()) + b.Use(logger.New(log)) + + b. + b.Handle(tele.OnText, func(c tele.Context) error { + return c.Reply(c.Get("trace_id").(string)) + }) + return &App{log: log, bot: b} +} + +func (a *App) Run() { + a.bot.Start() +} + +func (a *App) Stop() { + a.bot.Stop() +} diff --git a/internal/lib/logger/handlers/slogpretty/slogpretty.go b/internal/lib/logger/handlers/slogpretty/slogpretty.go new file mode 100644 index 0000000..120b2de --- /dev/null +++ b/internal/lib/logger/handlers/slogpretty/slogpretty.go @@ -0,0 +1,256 @@ +package slogpretty + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "strconv" + "strings" + "sync" +) + +const ( + timeFormat = "2006-01-02 15:04:05.000" + + reset = "\033[0m" + + black = 30 + red = 31 + green = 32 + yellow = 33 + blue = 34 + magenta = 35 + cyan = 36 + lightGray = 37 + darkGray = 90 + lightRed = 91 + lightGreen = 92 + lightYellow = 93 + lightBlue = 94 + lightMagenta = 95 + lightCyan = 96 + white = 97 +) + +func colorizer(colorCode int, v string) string { + return fmt.Sprintf("\033[%sm%s%s", strconv.Itoa(colorCode), v, reset) +} + +type Handler struct { + h slog.Handler + r func([]string, slog.Attr) slog.Attr + b *bytes.Buffer + m *sync.Mutex + writer io.Writer + colorize bool + outputEmptyAttrs bool +} + +func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { + return h.h.Enabled(ctx, level) +} + +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &Handler{h: h.h.WithAttrs(attrs), b: h.b, r: h.r, m: h.m, writer: h.writer, colorize: h.colorize} +} + +func (h *Handler) WithGroup(name string) slog.Handler { + return &Handler{h: h.h.WithGroup(name), b: h.b, r: h.r, m: h.m, writer: h.writer, colorize: h.colorize} +} + +func (h *Handler) computeAttrs( + ctx context.Context, + r slog.Record, +) (map[string]any, error) { + h.m.Lock() + defer func() { + h.b.Reset() + h.m.Unlock() + }() + if err := h.h.Handle(ctx, r); err != nil { + return nil, fmt.Errorf("error when calling inner handler's Handle: %w", err) + } + + var attrs map[string]any + err := json.Unmarshal(h.b.Bytes(), &attrs) + if err != nil { + return nil, fmt.Errorf("error when unmarshaling inner handler's Handle result: %w", err) + } + return attrs, nil +} + +func (h *Handler) Handle(ctx context.Context, r slog.Record) error { + colorize := func(code int, value string) string { + return value + } + if h.colorize { + colorize = colorizer + } + + var level string + levelAttr := slog.Attr{ + Key: slog.LevelKey, + Value: slog.AnyValue(r.Level), + } + if h.r != nil { + levelAttr = h.r([]string{}, levelAttr) + } + + var debugColor = white + if !levelAttr.Equal(slog.Attr{}) { + level = levelAttr.Value.String() + + if r.Level <= slog.LevelDebug { + level = colorize(lightBlue, level) // Debug level + debugColor = lightBlue + } else if r.Level <= slog.LevelInfo { + level = colorize(white, level) + " " + debugColor = white + } else if r.Level < slog.LevelWarn { + level = colorize(yellow, level) + debugColor = yellow + } else if r.Level < slog.LevelError { // Warn level + level = colorize(lightYellow, level) + " " + debugColor = lightYellow + } else if r.Level <= slog.LevelError+1 { + level = colorize(lightRed, level) + debugColor = lightRed + } else if r.Level > slog.LevelError+1 { + level = colorize(lightMagenta, level) + debugColor = lightMagenta + } + } + + var timestamp string + timeAttr := slog.Attr{ + Key: slog.TimeKey, + Value: slog.StringValue(r.Time.Format(timeFormat)), + } + if h.r != nil { + timeAttr = h.r([]string{}, timeAttr) + } + if !timeAttr.Equal(slog.Attr{}) { + timestamp = colorize(lightGreen, timeAttr.Value.String()) + } + + var msg string + msgAttr := slog.Attr{ + Key: slog.MessageKey, + Value: slog.StringValue(r.Message), + } + if h.r != nil { + msgAttr = h.r([]string{}, msgAttr) + } + if !msgAttr.Equal(slog.Attr{}) { + msg = colorize(debugColor, msgAttr.Value.String()) + } + + attrs, err := h.computeAttrs(ctx, r) + if err != nil { + return err + } + + var attrsAsBytes []byte + if h.outputEmptyAttrs || len(attrs) > 0 { + if r.Level >= slog.LevelWarn { + attrsAsBytes, err = json.MarshalIndent(attrs, "", " ") + } else { + attrsAsBytes, err = json.Marshal(attrs) + } + if err != nil { + return fmt.Errorf("error when marshaling attrs: %w", err) + } + } + + out := strings.Builder{} + if len(timestamp) > 0 { + out.WriteString(timestamp) + out.WriteString(" | ") + } + if len(level) > 0 { + out.WriteString(level) + out.WriteString(" | ") + } + if len(msg) > 0 { + out.WriteString(msg) + out.WriteString(" ") + } + if len(attrsAsBytes) > 0 { + out.WriteString(colorize(darkGray, string(attrsAsBytes))) + } + + _, err = io.WriteString(h.writer, out.String()+"\n") + if err != nil { + return err + } + + return nil +} + +func suppressDefaults( + next func([]string, slog.Attr) slog.Attr, +) func([]string, slog.Attr) slog.Attr { + return func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey || + a.Key == slog.LevelKey || + a.Key == slog.MessageKey { + return slog.Attr{} + } + if next == nil { + return a + } + return next(groups, a) + } +} + +func New(handlerOptions *slog.HandlerOptions, options ...Option) *Handler { + if handlerOptions == nil { + handlerOptions = &slog.HandlerOptions{} + } + + buf := &bytes.Buffer{} + handler := &Handler{ + b: buf, + h: slog.NewJSONHandler(buf, &slog.HandlerOptions{ + Level: handlerOptions.Level, + AddSource: handlerOptions.AddSource, + ReplaceAttr: suppressDefaults(handlerOptions.ReplaceAttr), + }), + r: handlerOptions.ReplaceAttr, + m: &sync.Mutex{}, + } + + for _, opt := range options { + opt(handler) + } + + return handler +} + +func NewHandler(opts *slog.HandlerOptions) *Handler { + return New(opts, WithDestinationWriter(os.Stdout), WithColor(), WithOutputEmptyAttrs()) +} + +type Option func(h *Handler) + +func WithDestinationWriter(writer io.Writer) Option { + return func(h *Handler) { + h.writer = writer + } +} + +func WithColor() Option { + return func(h *Handler) { + h.colorize = true + } +} + +func WithOutputEmptyAttrs() Option { + return func(h *Handler) { + h.outputEmptyAttrs = true + } +} diff --git a/internal/lib/logger/sl/sl.go b/internal/lib/logger/sl/sl.go new file mode 100644 index 0000000..ac19308 --- /dev/null +++ b/internal/lib/logger/sl/sl.go @@ -0,0 +1,10 @@ +package sl + +import "log/slog" + +func Err(err error) slog.Attr { + return slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + } +} diff --git a/internal/telegram/middleware/logger/logger.go b/internal/telegram/middleware/logger/logger.go new file mode 100644 index 0000000..5229302 --- /dev/null +++ b/internal/telegram/middleware/logger/logger.go @@ -0,0 +1,36 @@ +package logger + +import ( + "fmt" + tele "gopkg.in/telebot.v4" + "log/slog" +) + +func New(log *slog.Logger) tele.MiddlewareFunc { + return func(next tele.HandlerFunc) tele.HandlerFunc { + log = log.With( + slog.String("component", "middleware/logger"), + ) + + return func(c tele.Context) error { + if msg := c.Message(); msg != nil { + log.Info("incoming message", + slog.Int64("from_id", c.Sender().ID), + slog.String("from", c.Sender().Username), + slog.String("full_name", fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName)), + slog.String("message", msg.Text), + ) + } + if cb := c.Callback(); cb != nil { + log.Info("incoming callback", + slog.Int64("from_id", c.Sender().ID), + slog.String("from", c.Sender().Username), + slog.String("full_name", fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName)), + slog.String("message", cb.Data), + ) + } + + return next(c) + } + } +} diff --git a/internal/telegram/middleware/trace/trace.go b/internal/telegram/middleware/trace/trace.go new file mode 100644 index 0000000..962060f --- /dev/null +++ b/internal/telegram/middleware/trace/trace.go @@ -0,0 +1,32 @@ +package trace + +import ( + "fmt" + "github.com/google/uuid" + tele "gopkg.in/telebot.v4" + "log/slog" +) + +// New +// Generates a unique trace_id for each request, which can be accessed from the context like this: +// var context tele.Context +// traceID := context.Get("trace_id") +func New(log *slog.Logger) tele.MiddlewareFunc { + return func(next tele.HandlerFunc) tele.HandlerFunc { + log = log.With( + slog.String("component", "middleware/trace"), + ) + + return func(c tele.Context) error { + newUUID, err := uuid.NewUUID() + if err != nil { + log.Warn("failed to generate UUID") + } + traceID := fmt.Sprintf("%d/%s/%s", c.Sender().ID, c.Sender().Username, newUUID.String()) + + c.Set("trace_id", traceID) + + return next(c) + } + } +} diff --git a/test_v2/mocks/mockgen.go b/test_v2/mocks/mockgen.go new file mode 100644 index 0000000..f726b26 --- /dev/null +++ b/test_v2/mocks/mockgen.go @@ -0,0 +1 @@ +package mocks From c9f8785afa520e969472de32bac48528fbe02b2c Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 28 Mar 2025 12:49:04 +0300 Subject: [PATCH 03/44] add fsm machine --- go.mod | 3 +++ go.sum | 3 ++- internal/lib/fsm/fsm.go | 9 +++++++++ internal/lib/fsm/memory/fsm.go | 34 ++++++++++++++++++++++++++++++++++ test_v2/lib/fsm/memory_test.go | 28 ++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 internal/lib/fsm/fsm.go create mode 100644 internal/lib/fsm/memory/fsm.go create mode 100644 test_v2/lib/fsm/memory_test.go diff --git a/go.mod b/go.mod index 47ca611..ba191f9 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/jxskiss/base62 v1.1.0 github.com/ncruces/go-sqlite3 v0.22.0 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.5 gopkg.in/telebot.v4 v4.0.0-beta.4 @@ -18,7 +19,9 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index a20df29..e0e9f1b 100644 --- a/go.sum +++ b/go.sum @@ -383,8 +383,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= diff --git a/internal/lib/fsm/fsm.go b/internal/lib/fsm/fsm.go new file mode 100644 index 0000000..2c8fe97 --- /dev/null +++ b/internal/lib/fsm/fsm.go @@ -0,0 +1,9 @@ +package fsm + +type State int + +const ( + Default State = iota + SendingCookie + ChattingAI +) diff --git a/internal/lib/fsm/memory/fsm.go b/internal/lib/fsm/memory/fsm.go new file mode 100644 index 0000000..195353d --- /dev/null +++ b/internal/lib/fsm/memory/fsm.go @@ -0,0 +1,34 @@ +package memory + +import ( + "algobot/internal/lib/fsm" + "sync" +) + +type Memory struct { + mu sync.Mutex + statements map[int64]fsm.State +} + +func New() *Memory { + return &Memory{ + statements: make(map[int64]fsm.State), + } +} +func (m *Memory) SetState(uid int64, statement fsm.State) { + m.mu.Lock() + defer m.mu.Unlock() + + m.statements[uid] = statement +} +func (m *Memory) State(uid int64) fsm.State { + m.mu.Lock() + defer m.mu.Unlock() + + v, ok := m.statements[uid] + if ok { + return v + } + m.statements[uid] = fsm.Default + return m.statements[uid] +} diff --git a/test_v2/lib/fsm/memory_test.go b/test_v2/lib/fsm/memory_test.go new file mode 100644 index 0000000..75aaeef --- /dev/null +++ b/test_v2/lib/fsm/memory_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "algobot/internal/lib/fsm" + "algobot/internal/lib/fsm/memory" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Memory(t *testing.T) { + t.Run("Set state", func(t *testing.T) { + sm := memory.New() + + sm.SetState(1, fsm.Default) + got := sm.State(1) + assert.Equal(t, fsm.Default, got) + + sm.SetState(1, fsm.ChattingAI) + got = sm.State(1) + assert.Equal(t, fsm.ChattingAI, got) + }) + t.Run("Get state if not exists", func(t *testing.T) { + sm := memory.New() + + got := sm.State(1) + assert.Equal(t, fsm.Default, got) + }) +} From 19a949d5399addacbb3ece9a678d26d7569ad510 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 28 Mar 2025 16:15:53 +0300 Subject: [PATCH 04/44] add handler dispatcher + test --- go.mod | 1 + go.sum | 2 + internal/app/telegram/app.go | 16 +++++- internal/lib/fsm/memory/fsm.go | 2 + .../telegram/dispatcher/text/dispatcher.go | 35 ++++++++++++ .../handlers/text/chatting-ai/chattingAI.go | 18 ++++++ .../handlers/text/default-state/default.go | 20 +++++++ .../text/sending-cookie/sendingCookie.go | 18 ++++++ test_v2/mocks/handler_mock.go | 55 +++++++++++++++++++ test_v2/mocks/mockgen.go | 3 + test_v2/mocks/slog_mock.go | 32 +++++++++++ .../telegram/dispatcher/dispatcher_test.go | 34 ++++++++++++ 12 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 internal/telegram/dispatcher/text/dispatcher.go create mode 100644 internal/telegram/handlers/text/chatting-ai/chattingAI.go create mode 100644 internal/telegram/handlers/text/default-state/default.go create mode 100644 internal/telegram/handlers/text/sending-cookie/sendingCookie.go create mode 100644 test_v2/mocks/handler_mock.go create mode 100644 test_v2/mocks/slog_mock.go create mode 100644 test_v2/telegram/dispatcher/dispatcher_test.go diff --git a/go.mod b/go.mod index ba191f9..70c3b7b 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index e0e9f1b..ce77622 100644 --- a/go.sum +++ b/go.sum @@ -421,6 +421,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index c007973..fc58fab 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -1,7 +1,9 @@ package telegram import ( + "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" + dispatcher2 "algobot/internal/telegram/dispatcher/text" "algobot/internal/telegram/middleware/logger" "algobot/internal/telegram/middleware/trace" tele "gopkg.in/telebot.v4" @@ -41,10 +43,18 @@ func New(log *slog.Logger, token string) *App { b.Use(middleware.Recover()) b.Use(logger.New(log)) - b. - b.Handle(tele.OnText, func(c tele.Context) error { - return c.Reply(c.Get("trace_id").(string)) + dispatcher := dispatcher2.NewDispatcher(log) + + state := memory.New() + + b.Handle(tele.OnText, func(c tele.Context) error { + userState := state.State(c.Sender().ID) + + handler := dispatcher.GetHandlers(userState) + + return handler.Handle(c) }) + return &App{log: log, bot: b} } diff --git a/internal/lib/fsm/memory/fsm.go b/internal/lib/fsm/memory/fsm.go index 195353d..3a49f82 100644 --- a/internal/lib/fsm/memory/fsm.go +++ b/internal/lib/fsm/memory/fsm.go @@ -15,12 +15,14 @@ func New() *Memory { statements: make(map[int64]fsm.State), } } + func (m *Memory) SetState(uid int64, statement fsm.State) { m.mu.Lock() defer m.mu.Unlock() m.statements[uid] = statement } + func (m *Memory) State(uid int64) fsm.State { m.mu.Lock() defer m.mu.Unlock() diff --git a/internal/telegram/dispatcher/text/dispatcher.go b/internal/telegram/dispatcher/text/dispatcher.go new file mode 100644 index 0000000..83c46ba --- /dev/null +++ b/internal/telegram/dispatcher/text/dispatcher.go @@ -0,0 +1,35 @@ +package text + +import ( + "algobot/internal/lib/fsm" + tele "gopkg.in/telebot.v4" + "log/slog" +) + +type Dispatcher struct { + log *slog.Logger + handler Handlers +} + +type Handler interface { + Handle(c tele.Context) error +} + +type Handlers map[fsm.State]Handler + +func NewDispatcher(log *slog.Logger) *Dispatcher { + + return &Dispatcher{log: log, handler: make(Handlers)} +} + +func (d *Dispatcher) Register(state fsm.State, handler Handler) { + d.handler[state] = handler +} + +func (d *Dispatcher) GetHandlers(state fsm.State) Handler { + if val, ok := d.handler[state]; ok { + return val + } + + return d.handler[fsm.Default] // TODO : refactor default val, change to ret error +} diff --git a/internal/telegram/handlers/text/chatting-ai/chattingAI.go b/internal/telegram/handlers/text/chatting-ai/chattingAI.go new file mode 100644 index 0000000..0afccf5 --- /dev/null +++ b/internal/telegram/handlers/text/chatting-ai/chattingAI.go @@ -0,0 +1,18 @@ +package chattingAI + +import ( + "gopkg.in/telebot.v4" + "log/slog" +) + +type ChattingAI struct { + log *slog.Logger +} + +func New(log *slog.Logger) *ChattingAI { + return &ChattingAI{} +} + +func (d ChattingAI) Handle(c telebot.Context) error { + panic("implement me") +} diff --git a/internal/telegram/handlers/text/default-state/default.go b/internal/telegram/handlers/text/default-state/default.go new file mode 100644 index 0000000..6e46d7c --- /dev/null +++ b/internal/telegram/handlers/text/default-state/default.go @@ -0,0 +1,20 @@ +package defaultstate + +import ( + "gopkg.in/telebot.v4" + "log/slog" +) + +type DefaultState struct { + log *slog.Logger +} + +func New(log *slog.Logger) *DefaultState { + return &DefaultState{ + log: log, + } +} + +func (d *DefaultState) Handle(c telebot.Context) error { + panic("implement me") +} diff --git a/internal/telegram/handlers/text/sending-cookie/sendingCookie.go b/internal/telegram/handlers/text/sending-cookie/sendingCookie.go new file mode 100644 index 0000000..041287d --- /dev/null +++ b/internal/telegram/handlers/text/sending-cookie/sendingCookie.go @@ -0,0 +1,18 @@ +package sendingCookie + +import ( + "gopkg.in/telebot.v4" + "log/slog" +) + +type SendingCookie struct { + log *slog.Logger +} + +func New(log *slog.Logger) *SendingCookie { + return &SendingCookie{} +} + +func (d SendingCookie) Handle(c telebot.Context) error { + panic("implement me") +} diff --git a/test_v2/mocks/handler_mock.go b/test_v2/mocks/handler_mock.go new file mode 100644 index 0000000..e17b8a9 --- /dev/null +++ b/test_v2/mocks/handler_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: algobot/internal/telegram/dispatcher/text (interfaces: Handler) +// +// Generated by this command: +// +// mockgen -destination=./handler_mock.go -package=mocks algobot/internal/telegram/dispatcher/text Handler +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + telebot_v4 "gopkg.in/telebot.v4" +) + +// MockHandler is a mock of Handler interface. +type MockHandler struct { + ctrl *gomock.Controller + recorder *MockHandlerMockRecorder + isgomock struct{} +} + +// MockHandlerMockRecorder is the mock recorder for MockHandler. +type MockHandlerMockRecorder struct { + mock *MockHandler +} + +// NewMockHandler creates a new mock instance. +func NewMockHandler(ctrl *gomock.Controller) *MockHandler { + mock := &MockHandler{ctrl: ctrl} + mock.recorder = &MockHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHandler) EXPECT() *MockHandlerMockRecorder { + return m.recorder +} + +// Handle mocks base method. +func (m *MockHandler) Handle(c telebot_v4.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Handle", c) + ret0, _ := ret[0].(error) + return ret0 +} + +// Handle indicates an expected call of Handle. +func (mr *MockHandlerMockRecorder) Handle(c any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockHandler)(nil).Handle), c) +} diff --git a/test_v2/mocks/mockgen.go b/test_v2/mocks/mockgen.go index f726b26..69d1ab3 100644 --- a/test_v2/mocks/mockgen.go +++ b/test_v2/mocks/mockgen.go @@ -1 +1,4 @@ package mocks + +// Dispatcher handler +//go:generate mockgen -destination=./handler_mock.go -package=mocks algobot/internal/telegram/dispatcher/text Handler diff --git a/test_v2/mocks/slog_mock.go b/test_v2/mocks/slog_mock.go new file mode 100644 index 0000000..f58eaf1 --- /dev/null +++ b/test_v2/mocks/slog_mock.go @@ -0,0 +1,32 @@ +package mocks + +import ( + "context" + "log/slog" +) + +func NewMockLogger() *slog.Logger { + return slog.New(NewDiscardHandler()) +} + +type DiscardHandler struct{} + +func NewDiscardHandler() *DiscardHandler { + return &DiscardHandler{} +} + +func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { + return nil +} + +func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h *DiscardHandler) WithGroup(_ string) slog.Handler { + return h +} + +func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { + return false +} diff --git a/test_v2/telegram/dispatcher/dispatcher_test.go b/test_v2/telegram/dispatcher/dispatcher_test.go new file mode 100644 index 0000000..b5ecd86 --- /dev/null +++ b/test_v2/telegram/dispatcher/dispatcher_test.go @@ -0,0 +1,34 @@ +package test + +import ( + "algobot/internal/lib/fsm" + "algobot/internal/telegram/dispatcher/text" + "algobot/test_v2/mocks" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +func Test_Dispatcher(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + h1 := mocks.NewMockHandler(ctrl) + h2 := mocks.NewMockHandler(ctrl) + h3 := mocks.NewMockHandler(ctrl) + + log := mocks.NewMockLogger() + + dispather := text.NewDispatcher(log) + dispather.Register(fsm.Default, h1) + dispather.Register(fsm.SendingCookie, h2) + dispather.Register(fsm.ChattingAI, h3) + + handler := dispather.GetHandlers(fsm.Default) + assert.Same(t, h1, handler) + handler = dispather.GetHandlers(fsm.SendingCookie) + assert.Same(t, h2, handler) + handler = dispather.GetHandlers(fsm.ChattingAI) + assert.Same(t, h3, handler) + +} From 7ac2a76fe947837c8aa32ff0b4bccf3c07989dfa Mon Sep 17 00:00:00 2001 From: danil227pavlov Date: Tue, 1 Apr 2025 21:48:03 +0300 Subject: [PATCH 05/44] add start handler + tests --- .gitignore | 3 +- cmd/icon.ico | Bin 9976 -> 0 bytes cmd/main.go | 107 ---- cmd/migrations/createGroups.sql | 8 - cmd/migrations/createUsers.sql | 9 - go.mod | 10 +- go.sum | 62 +-- internal/app/telegram/app.go | 21 +- internal/domain/telegram/keyboards/start.go | 20 + .../telegram/dispatcher/text/dispatcher.go | 35 -- .../handlers/text/chatting-ai/chattingAI.go | 18 - .../handlers/text/default-state/default.go | 20 - .../text/sending-cookie/sendingCookie.go | 18 - internal/telegram/handlers/text/start.go | 19 + internal/telegram/middleware/stater/stater.go | 23 + internal_old/clients/backoffice.go | 305 ----------- internal_old/clients/webClient.go | 323 ----------- internal_old/config/keyboards.go | 50 -- internal_old/config/texts.go | 34 -- .../callbackHandlers/changeNotification.go | 39 -- .../callbackHandlers/closeLesson.go | 48 -- .../callbackHandlers/getCredentials.go | 50 -- .../callbackHandlers/openLesson.go | 48 -- .../callbackHandlers/refreshGroups.go | 42 -- .../callbackHandlers/setCookie.go | 31 -- .../contextHandlers/defaultHandler/handler.go | 8 - .../handlersHolders/DefaultCBHolder.go | 32 -- .../handlersHolders/DefaultHolder.go | 33 -- .../handlersHolders/SendingCookie.go | 28 - .../handlersHolders/chattingAI.go | 34 -- .../contextHandlers/handlersHolders/holder.go | 11 - internal_old/contextHandlers/onCallback.go | 58 -- internal_old/contextHandlers/onText.go | 56 -- .../textHandlers/chattingAi/AnyMessage.go | 37 -- .../textHandlers/chattingAi/ClearHistory.go | 32 -- .../textHandlers/chattingAi/backAction.go | 27 - .../textHandlers/defaultState/absentKids.go | 66 --- .../textHandlers/defaultState/aiChat.go | 29 - .../textHandlers/defaultState/missingKids.go | 98 ---- .../textHandlers/defaultState/myGroups.go | 88 --- .../textHandlers/defaultState/settings.go | 59 -- .../textHandlers/defaultState/start.go | 19 - .../defaultState/startWithPayload.go | 140 ----- .../sendingCookieState/rejectAction.go | 26 - .../sendingCookieState/sendCookie.go | 38 -- internal_old/domain/domain.go | 34 -- internal_old/domain/sqlite3.go | 349 ------------ internal_old/error/error.go | 10 - internal_old/helpers/group.go | 68 --- internal_old/helpers/group_test.go | 100 ---- internal_old/helpers/logError.go | 32 -- internal_old/middleware/logger.go | 18 - internal_old/middleware/register.go | 38 -- internal_old/models/models.go | 81 --- internal_old/models/startPayload.go | 11 - internal_old/schedulers/message.go | 63 --- internal_old/serdes/simple.go | 25 - internal_old/service/AIService.go | 55 -- internal_old/service/DefaultService.go | 428 --------------- internal_old/service/service.go | 27 - internal_old/stateMachine/memory.go | 31 -- internal_old/stateMachine/stateMachine.go | 18 - {test_v2 => test}/lib/fsm/memory_test.go | 0 test/mocks/mockgen.go | 1 + {test_v2 => test}/mocks/slog_mock.go | 0 test/mocks/telegram/handlers/mockgen.go | 3 + test/mocks/telegram/mockgen.go | 3 + test/telegram/handlers/start_test.go | 30 ++ test_v2/mocks/handler_mock.go | 55 -- test_v2/mocks/mockgen.go | 4 - .../telegram/dispatcher/dispatcher_test.go | 34 -- tests/clients/backoffice_test.go | 290 ---------- tests/domain/domain_test.go | 306 ----------- tests/handlers/chattingAI/chattingAI_test.go | 125 ----- .../defaultState/CallbackHandler_test.go | 148 ----- .../defaultState/DefaultHandler_test.go | 509 ------------------ .../sendCookieState/sendCookieHandler_test.go | 95 ---- tests/mockgen.go | 6 - tests/mocks/AIService_mock.go | 63 --- tests/mocks/MockContext.go | 73 --- tests/mocks/MockStateMachine.go | 24 - tests/mocks/mockBot.go | 16 - tests/mocks/mockDomain.go | 98 ---- tests/mocks/mockWebClient.go | 268 --------- tests/mocks/newMockService.go | 175 ------ tests/scheduler/message_test.go | 23 - tests/services/service_test.go | 267 --------- tests/stateMachine_test.go | 26 - 88 files changed, 117 insertions(+), 6175 deletions(-) delete mode 100644 cmd/icon.ico delete mode 100644 cmd/main.go delete mode 100644 cmd/migrations/createGroups.sql delete mode 100644 cmd/migrations/createUsers.sql create mode 100644 internal/domain/telegram/keyboards/start.go delete mode 100644 internal/telegram/dispatcher/text/dispatcher.go delete mode 100644 internal/telegram/handlers/text/chatting-ai/chattingAI.go delete mode 100644 internal/telegram/handlers/text/default-state/default.go delete mode 100644 internal/telegram/handlers/text/sending-cookie/sendingCookie.go create mode 100644 internal/telegram/handlers/text/start.go create mode 100644 internal/telegram/middleware/stater/stater.go delete mode 100644 internal_old/clients/backoffice.go delete mode 100644 internal_old/clients/webClient.go delete mode 100644 internal_old/config/keyboards.go delete mode 100644 internal_old/config/texts.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/changeNotification.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/closeLesson.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/getCredentials.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/openLesson.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/refreshGroups.go delete mode 100644 internal_old/contextHandlers/callbackHandlers/setCookie.go delete mode 100644 internal_old/contextHandlers/defaultHandler/handler.go delete mode 100644 internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go delete mode 100644 internal_old/contextHandlers/handlersHolders/DefaultHolder.go delete mode 100644 internal_old/contextHandlers/handlersHolders/SendingCookie.go delete mode 100644 internal_old/contextHandlers/handlersHolders/chattingAI.go delete mode 100644 internal_old/contextHandlers/handlersHolders/holder.go delete mode 100644 internal_old/contextHandlers/onCallback.go delete mode 100644 internal_old/contextHandlers/onText.go delete mode 100644 internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go delete mode 100644 internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go delete mode 100644 internal_old/contextHandlers/textHandlers/chattingAi/backAction.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/absentKids.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/aiChat.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/missingKids.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/myGroups.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/settings.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/start.go delete mode 100644 internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go delete mode 100644 internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go delete mode 100644 internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go delete mode 100644 internal_old/domain/domain.go delete mode 100644 internal_old/domain/sqlite3.go delete mode 100644 internal_old/error/error.go delete mode 100644 internal_old/helpers/group.go delete mode 100644 internal_old/helpers/group_test.go delete mode 100644 internal_old/helpers/logError.go delete mode 100644 internal_old/middleware/logger.go delete mode 100644 internal_old/middleware/register.go delete mode 100644 internal_old/models/models.go delete mode 100644 internal_old/models/startPayload.go delete mode 100644 internal_old/schedulers/message.go delete mode 100644 internal_old/serdes/simple.go delete mode 100644 internal_old/service/AIService.go delete mode 100644 internal_old/service/DefaultService.go delete mode 100644 internal_old/service/service.go delete mode 100644 internal_old/stateMachine/memory.go delete mode 100644 internal_old/stateMachine/stateMachine.go rename {test_v2 => test}/lib/fsm/memory_test.go (100%) create mode 100644 test/mocks/mockgen.go rename {test_v2 => test}/mocks/slog_mock.go (100%) create mode 100644 test/mocks/telegram/handlers/mockgen.go create mode 100644 test/mocks/telegram/mockgen.go create mode 100644 test/telegram/handlers/start_test.go delete mode 100644 test_v2/mocks/handler_mock.go delete mode 100644 test_v2/mocks/mockgen.go delete mode 100644 test_v2/telegram/dispatcher/dispatcher_test.go delete mode 100644 tests/clients/backoffice_test.go delete mode 100644 tests/domain/domain_test.go delete mode 100644 tests/handlers/chattingAI/chattingAI_test.go delete mode 100644 tests/handlers/defaultState/CallbackHandler_test.go delete mode 100644 tests/handlers/defaultState/DefaultHandler_test.go delete mode 100644 tests/handlers/sendCookieState/sendCookieHandler_test.go delete mode 100644 tests/mockgen.go delete mode 100644 tests/mocks/AIService_mock.go delete mode 100644 tests/mocks/MockContext.go delete mode 100644 tests/mocks/MockStateMachine.go delete mode 100644 tests/mocks/mockBot.go delete mode 100644 tests/mocks/mockDomain.go delete mode 100644 tests/mocks/mockWebClient.go delete mode 100644 tests/mocks/newMockService.go delete mode 100644 tests/scheduler/message_test.go delete mode 100644 tests/services/service_test.go delete mode 100644 tests/stateMachine_test.go diff --git a/.gitignore b/.gitignore index bd124b5..6045a68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /base.db .env /.idea -config/local.yaml \ No newline at end of file +config/local.yaml +*_mock.go \ No newline at end of file diff --git a/cmd/icon.ico b/cmd/icon.ico deleted file mode 100644 index e785eb67558771038d232f28d673d6a27c3257f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9976 zcmc(_XH*kk^e#LJ34~sy_a;aerAP}(6_J2+=_nuwNRu7{C?ZuXfYb;`k=~IS6a=J8 zM>S%jXR_@rwlsCY`6Z*r_=5sV!23k;4N zoIF8>Z1LZ9(j>`WK^WHV9n7^gjZX*XspLkmqrAZb* zJVWSV9{?C16TtTj%=-)hJfovP(cJ{m-4p?C!l3Z~J3<8Te>9Ct8r@n$?+qaxUBaS9 z^N@m6u?qLr$0}h0Sq)lckN?UgMd@x8Eb@4Ed)_ z)8Y*}3o516o?Gi0ZaE=f<#jc;wRl=*-nADfF$7#53Qs{^QlD+H517z0tQMvz@1{Ct z!t{%xUI~a=9-AtkWNK}cZ)$ucBWV~-zEx%$Dd&VqeFnC8seXmY@kgHYC&!v5)LxHx z>(oeM3Xi*Xf0Wj!!j1V$e(h3Ci(h!%7aCw8b>TH}pp z;cWpi((lsw^k=UrFsE7iE&g?Xhpt4~Q`mBlS+=rbWYs)0N{hLW+E$}neBhMl)tnS) zZwNX*zIa~7t$kSz<`{bF0_%>2OLmWAZ~t?Ks6jCfw|UiEaF{wt!nl0wjkl=} zIZflgVZ3Stz;kcYlf`4Qz)?;jilOBC%~r4G#-tgd!}~kM%(TT>1nIi#8JC zgzKe3f$c1!0lT~8*>2oleQ4#rZfz{E!iD@uZVo`!HWi+CcZGg3aPDJRVgh z(KVIQIg7Kd!|#0S?*W%0Xa4-!=i?%UcPjG9P9nNP!CLIgnTM+71qF5`1INK z_V~5962(3PP=&Y+(0CZv$De&-&v;Bjfd)f{N+I};G}4fR5KD>U_nv`LP|VdffXZrf zz<%Yc1pZ~K8Wh0?E_^D7>tzWR0g@jkz`z14J4B-~A)O<@g{+_k_U&JK#7R^w@(8T# zPVN>H$6bJ6s8A0rM#7l@DCQ;z0WblSP&mlA5QrI36c=a0=!*h%c=2=Tmvb@k)c@Zh z@gy8nao+oXjzVnyKLiNT+RsL9O2mc;fU@wz(~W|uWeHza3toSVuY}K>q|@HfJpKGT zgY?%pSv%hUm6{(_B2j29=jp<3b`|+c<;>^u)r2XA@AYs-HF~FVcIQfeDsH;ezg7An zz-ErR08Bx|(8+b6}o@N46V~+k)i3+6}v3X+cSymmHTr}kS&)e z&rrsY^|blY7|goI^YTyBbF~uqGD?9b!5SgfkinA%?sDbvXp;Mj3K9@{wIH_KotXWM zKMX1}@cU(=00nvnVyAkChE>5|E;iTmV7~P1QF|TP7rduSyTGMRy^ z?@og50Zj0RFwCb9Y09;AsQf|6g{?7i<m|{DcMp+nngxm&5jYr*i zn>picBZC-k1ZE{JXMFaVg#X0iK30BlsfmrsSwk(?ZnoB@XX@NeC7TY=RM@e7QcQM0 z8(Gt7lSST!{qSw=d4Iu@pGn^%m1}Y6`ZKjojJ)V~-dxoKCC5TP4Mfwtg3amJqm0mU zmNzS%j3d4L(O@(nf_lfgB|BO>_D>J0q53&mrOksy6e-YLX1X1GxfGL8EM$8{l;dE~ zIp5aDjOzLTETn$i@r==HPc8v89)D*y!NTYqG!@a`Mc5}=lgUGvqV0FBsdEf?Ov2gH z{`i^8fW)mBs)BKTwRT5|=|g+NLQ5~oc|k;DqXA|F+b^mNSU%qQB;faoJwb)u&%`fN zDSqfvL(%vKfA&WO$$gS9 zc%}3Zdf(NA96tYAmP(YbNb;GS+&twr_XDy#i0^;BxQc_a9@U_PaN}2C(p)q zx=h5BBv51c#)8`r-g0N!par~{AO90)Mw&mH>h=$oA*2_GmvzmqjWL)=dc@i#*7iu_ zPvbpUzzvNjJ*dIAmD6)1d?ca(-zT(HFyc~Y^3l5C**p-Zs~GjDWb-4KsGlb-P&+2y z4ug1Hr?zHqTbZG8gCsqoYHTY@6L>#%84%-uH9UuWTELgOf2~$ij2$g9taU-?o-0WN zW#O9b!Zo7*W^y~$bTs;{?cK^AY%kU6Z94ex1wPSmxCzyDxAn@ugZ->c2!w9J2O{(U zsI>LglJEO~Pc8j#Cja$5H4e_fTgLk5o8tQ!j$`RD@61lDA4y;s<`}Pwh%|vq3tM(M zID?zW4hdHVz#;$zF36-gwnzNwS(ynWOsSb47wWNn)cvsOVQ+w5>ccy5qsvQ%1>7U~ zf>z@UxIh?l+rT3}u|~_=8CQ8?-(w_{Mbo(0H{&zPH{YqHH zm_M}gG4S80K>-L(_p204XGXYM7p*Mts=aM0rz};7=`YamYkfpuHB%iCqAGGar-2Ne zraJX@5~t2`HpM&|py>YFA%<jy?;OC0Y6icwacGRLy(ok1T1dJKkVvb4?S_VxYWy zn2u8t#tK~M^Iba$6g@s8Wy4Xf)9F^M@z=qfb!(9kO(^STf9A-oJfpQjoNUg#BCsja`=#ZNvlV_I@bCBp zQl}B7ZuM}cscy1-S-rVB;6#|=GR5C#3wL`2My$}oK;2XE{v_@qR2&O`$B8Zy=d=Z8 z5<;NLdPWsuD=7n;Sr}$feiHY{{O^$2Pt76g-!JrbS=L)!wMsm>pW`Z_?$NC*YZt_p zb$rYIt})zlJxQ39YBX#z(HS{ZGQa|3B4e;Fg3^Ge5#U!>^`gsk$m-4n<62jj)`C6H z5qBs7l9L5lmx1KGS!0~H54Y}XUYT>!YjbcOq(_zHi_`tQuE8ZzU_X<*IlGz z4QBjD>wV*Omo|Rk>&~mByW59O$<1 za&e2#&p4PFq6Z`a1Ff1uFTxhpV)e=h>)n;oToCo8Z|0DX;8$nwB7FmElP>cKR{RNf z3YZW~uGE<914rBh*Jhb=&b*Vm3^pAh@%bB$Ea1( zfa&dT*&OKj)nd0ux^qvTECad}awr%_PVJ1{y0y9)M3}K2Z$vSK1h ztymuOkxnG3=fSohsDwa_Md1-bfX@?ekt`N9gXr!li$-Wb6qV7g6No{vf{^lJFgotz zIf*l2W*E9f$yWuSoJh^Qf%?qn274deG$>9|vgP?l6ds$H_Pf?n>QNy>54^~~T2h9ot1^vBH-Vq)E=js7 zR9p|&=NQ0!w5`ple{@HGg{>1;-(V!xhS#ZfqBa&bqLUn=Q@a||?=Saq;&bn7xPvYj zrmzT`HAy@umohx}WluWUz@i|D#NOG!kVB&a7e(zcTn69KitC{V2yFg?FEgsv!63wQ z+mhxfQ^jt9{cwbKzsgxmCpji0`!yaL#H^p4(N>jxF*7yh<_3gL&6nwCkKbylS=W6J zvJYoNRqiF5mQqWz(Ed82YThSPQD-gV=Gd_=2-bkP;AbR>S31IP%!fpnGbu-lgHl-c>(07;5bHz-5#}o&!jJSDcwGkCE6geT)^e109MRv=O1e5b1)b{@b-962+lLv|+KGD3Gu%VKY6HSRV{zU%zl4Qb@)N>WydBNW+p+_lnLsZ{snY`WGz%pZBr zmzyi?-DI2A_Oj>C4!0Q(YOruJ9}1>AoAhkv>_Ngq7azUz<>b*AtL}81d%yU&%Bf?8 zdWnpBuik+7a7>dku$F}f+O)mAcy>x!SifFpTSW zUt(qXBUJ3?IB)~#0fE9aCm)k~I8r>~KKHIrg!1af`)^~#uuES4=~ajysk&bGs{&^g zF2V$}TFLLPd1QxzT!8Yk&!9O^9`*fcVR~h2yva+zO3d=H`t0-%Og-aHH0t2NXgD`A zP!%mef@S|VSqg*Er}`F`%aAB3(iZ(#>>#>eqIa49*j6<*2ufKD45_lt+xeTN(Lb#P z8Sm5>D!{Qw1fg%&%()5Wn3Ug64_Jn0UD>!AX-nE7ih4v&$ehjR>~LFUY5$#~=eJ__ zEficf${!h~3VN%HvuyJ}UsoqTLj!MCgcB<1Hv(%kA;Ia>+EVwu0)c2-KCo%t`da12 z9*&LdWl)U7Q>_-&t3zY!d6pqc@5OAms~RXZlX(F@QuTIW;7Is9o@)r-f{JXmz6m}O zu%%gePnJGrmlZgXH(bf17(|mIu{|zlaq)xG;>D{GL-y&m^p2E z0i)mMZ~DU=V7YbnvWnQUdX^al@`C%yMg|yt?w3YjN^z$myrTzdVT{!FhZck~1)rP8-Y0KqVRDSWYwlE87pV zvX*z@asnNJ<*B3GPd#Po znVX5*S_(hvM%Oja@L<^B#LMkn=z4}q=-~j+x%Ut0!*goihh&Wo$WU;$8+4Q?^m^%m zQYs?W7l`JnxCUbS=GT1oz)IqvO5SHKN1wO{b zkYhMC`mBF!tBbs}G)VXBm;VntW$xZ5XST@CHInepz;S&IjwDPhPR8+}EI|Jzp2tQ5 zCpmoB`xS1s)RIF)2@Tl6fj2P)$!HT!1&1uBiP!{4$Ho2$ykq;vIw3R*Yx6sPDsh3~ zOmBkd-1}(n<9xavuYmr`-TKn<1>{TYJ@t(zv|kc#V5{(g-N0C)$olx2-6MJ4r8xWDZ^E3|iNr66ZOd(4`dy3Fo3O@YGnWOw z;cNXtOxbTh<&wYC5g>)R`z(qQCB#84*vHhaul+q|PI zs~5h!!Lqg#?Qs~`R7Z{Uui0Zr5GBuBz69^4IDVxMESF)JLijlf{p!&ki)@e2dMq{q zOW1 znDJl0hjNNeFw15MrwQ?J*BWZ{ky@?8HoR$cU`9OHERTOVp=IjuIj-JhcC(oK*L8=5 z-JPvi5ZBuy3Qi|Ip0w|hMy~btnb(h`&2SSJ5m*Ib3YC-vwmH>=s3t-CnBd+P<+?7( z){(n1voQws2e*OP)3g5w%e<6v|Fi?!;e#E@lLURv0#}f2kJ^xEr(LMFxKT#OxTc=l z^M%>o6w3&-fgPM`O*(?Tyrk2A%-MB;nj%dbvU~yL@zpG31)GgQN}xF5>ix4{Fih*D zOOW){+}3q z!W2*KB7XN)fNQQYod&FZtQ1yQ+||TFP`;RPs=F6ZLmqf%68iYFd|czFhDHv25RBJ_ z9HYD^z*qH}`S{ldX$P#8mS-sQ&&{o82Fmi;u#jwh@P9>&WK{_fv-MwHz8S~@+QAS5 zUouX^`^*)bpg#H36d9gOS!eKZnPk9(+ssx);iJ%afv6#CiOHh^ zVy0mgrD{p_qvSO_pD(ub?@KDotXP&oB8=t@>Sl;kl3|~LWhvs=S3Bmy%%5HDz0#5D z5WC|g*xa_i!@B=3adl&ejgiT>S2OwRm8DZ=903d?(G$6Mb?mG8CAwjaohv1ndQ126 zZW>o*nbFyan3gdwjaF8zgPgLE2QK6PfF0VKcYa6d601Q8`t^d9zt8dz)>{2-darkP zIwa^rnlsq|qB_Ox9wWNpBS}rh@Z~*7YtXi`4YG#%RB5mfTkE8+)a>SfkF8NbTOK#O z>;tUwfl(*E7u_@-^lZ;CQW5n}^;0bUEBw5rkMhdI~4i+8&}25KEzTrdPWf z=V|l%Yg78dJF@MdG5WVY5$}{rof9ljEK7eSg_2 zSCS-~&Cb6j}fMmW+NWS8J=TLN<22Cq|Gr@i;o+OEf6dDx|aH$B?kE zSd+-Rq(zm1ek1I`q#(Elq9h*4NSE9Z9) z!Tj?}o@XA!9ok&X-TAM9^?`+zYz5q@6fZ;q3z|ZO8y@VI-^33d`JAUp@=d@-?Vf9> zo+GHV3`yOv=~i)`P6#t#G7KwTiF{_i;PL-aG5`P8y8rirC-*4P>0rALfQJ_7v9}x! zM>#w0Wk#&d|A0ydwemuSbU`J^t7`#nLV&h_YM2d-cM#`n5d}a5RG=HkJQ7thC;|ko zXao|3IcMe^xjnnSRK-!*iM;Eir0q$6|C$M|3B$aD$5*qFtuaUon-^Fl^MMSP$iEXw zDI8pwDv4_R)(9o?Ox!jy{!-dQi>uK{Srl%zV7`8~M+a2mL>?{Qy}p4N-^$eT40!8^ z64y&SP`m7mGc zlndlbI(`JMh)@x$2u-|XojyQ|Dgz8v{6R6l0 zw50u;?r&95A&vlc%ztcDg#WF3>hMPF@htid$aoTEM=_Q~x)GWKU0jK!jEI@AuB>>Q zve2)Sc3834{cvpqcXk&=_J zzf}2mTaKVo3i~_)dwJT%MS(UVg;E9UDeNaP9r4<>F|g#vIejQjw=vT>^kfx@V=p{I z(+;ZgMylmoA}wBspl%NUdBBVpL0N`S>`-{{Z2L#9#42Gzy2k4#deD`N(flLJs`!%b z;3;jH=x$q2&XIiP`9tT5Ioh>e`J_ zz!}fROkV;exRkB(C&<&+;d6&x$GcOIx>+n$w%Cmo6c6;tEf}v2Kai~?G`Gj(nvq#( zjg460cKs5*a$_ib`nfPLGR@*HKx*P{<^x>Bl0xk$#X z2SX#u6z+K?FDhIic5fxoqN=*L%5o=$p61cMn2VnO>TW&BBhJv|)Xcd0#kTnV_94&L z$?YeySQ4L0V;0dc@8Na>kTHW~6NF*81jR3;lMVwB3QlbvP!cI$(}~xg zDM-^*Wc%~<^J`_7+#^f_RP2*7VSJ(GhMDe9_^x{NTd|K=`7^Fw(WiGWT{Uuk2iXz0 zVR;%zU#>bHgR^!D8q;~~DGj&a*`fvsk3V(=pRHS?& zRMM4a$!R<@;r=@9P{~5n7-|?DV{f?9@GUx<$2)ECGHxScaO4>m?e(0Kp@*z*h~Z?! zn3@QOY=b1YI~JGytjyq(=@~9|w(w_wR&$m+zd6I?hOT3^k|?s^*owQG7O z=C_Ht+a*!dk}_ClP!xr`LI+1Y5MpRkiYW*iipvkPijcWNCRRP}kbf0qHPQj!`}=EG zdM78bbpE8^2Y!y(^xmWJ>kENO3x$krkzd|hJqLFaJu&e~8C(}LBH9SC&p@sy*BJI& zPL~Xtk9zwFArADZxgv{{55~SBe2P~aqfN2FdQ`1v(R>ZUGI}^-B8mCL2>)~kC}IFv z1slnM1oL6$3@~Id_Y2z|J8yV3*rvJ>Vp@+$Tgm#;KtY1sagHYYT(wi%rG#jyqqZ?1 zt#x~ipM8xO3_}`C9Q%C)JZ^xDul5*;vvqG%wKXxG{^WRK=f;q$k`@Sx`A+J6g-hP- z^E+L1hNgc0&q&)-arFWC!V=8p@F8Wh$)9nwYyFL+A`$ElEMMo}w_sj9X3SfNIDJDC zuI+l<-)2!Nq3V3dp6)$JqmyKiXq6R+_1kE48~Z+s8h=m#M2Xu~yR?Z!b$ccx;VrK_ zBis-yN=l3pO*H`7s&EDH`gFxsn8T{)yOC%7@Y?1;fgdpxc9!77yzr>R7xr|5?XfUD32IGroDe1FI@Ct|49*$#j z9v^j45g!22w#R#WSnn;)s;t#m$>JqAL$*YKs+-`{#2?{(?Ape8Oa0N@?2x6a4)4gj zH{k;t)f~x?d}am-utWkEfkxYU&#oaPm>1$DXAcwOvOeJQ&Y3eX_st=H)-i<}YtjOBUZ zAF84MZXm<5cQPXTyx(;exG3B8D?H6&abYb#B9GCELN3uBD2lbBGR#K1XLsn1>oEpy zdn9Oc6iVi*Qo0bFpP%qe@*Vse^YMA?Qq}}KxV?F)sL}HlYqnZ8{fWPiDdyE8c#SFJ z=IVs?1nQR@Kw)MHw4@ujwe68h=>Ld%JfT2@ZaG*;DLz~xKq7R z@YsaD`CpS{UP8i6j`2RU*?YK0+G9}eZ{>uRKyCxIp}abEvhNXCN#G{v6@|J-Fm;0x zen};sFqE!w5*(?+Iha$x7rS{VK1~}xziuXJHyBp`)!!|*k!s{}Bi%2eCqn>v5kkBZ zv;@vfd}*Rx?}`26(DMrJJz8ugb>X%KVtvCne9K4h(z!-Fq{&mKP|C~_q~!&aHvTLj zlEVv43-}w1$pFKJd~)o0+54Y)zq~9|!?C32xt;f$j5jzb%JZTg?|Bmt@qMgw<50JZ oAj()oo|6Uszj#MW=Txm`_iNczebZaSm(~DX&AS?9H|-++7rGeb=Kufz diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 2441985..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "algobot/internal_old/clients" - "algobot/internal_old/contextHandlers" - "algobot/internal_old/domain" - "algobot/internal_old/middleware" - "algobot/internal_old/schedulers" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "database/sql" - "embed" - "github.com/joho/godotenv" - _ "github.com/ncruces/go-sqlite3/driver" - _ "github.com/ncruces/go-sqlite3/embed" - tele "gopkg.in/telebot.v4" - middleware2 "gopkg.in/telebot.v4/middleware" - "log" - "os" - "time" -) - -//go:embed migrations/*.sql -var migrationsFS embed.FS - -func main() { - // TODO зарефачить main - godotenv.Load() - - TOKEN := os.Getenv("TELEGRAM_TOKEN") - PORT := os.Getenv("GRPC_PORT") - - db, closeDb := getSqliteBase("base.db") - defer closeDb() - - sqlite3 := domain.NewSqlite3(db) - sqlite3.Migrate(migrationsFS, "migrations") - - pref := tele.Settings{ - Token: TOKEN, - Poller: &tele.LongPoller{Timeout: 10 * time.Second}, - } - - b, err := tele.NewBot(pref) - if err != nil { - log.Fatal(err) - return - } - defer b.Stop() - os.Setenv("TELEGRAM_NAME", b.Me.Username) - - boClient := clients.NewBackoffice("", clients.BackofficeSetting{ - Retry: 3, - Timeout: 2 * time.Second, - RetryTimeout: 1 * time.Second, - }) - - svc := service.NewDefaultService(sqlite3, boClient) - state := stateMachine.NewMemory() - - regMid := middleware.NewRegister(svc) - aiService := service.NewAiService(PORT) - - msgHandler := contextHandlers.NewOnText(svc, state, aiService) - callbackHandler := contextHandlers.NewOnCallback(svc, state) - - tickerStop := goSchedule(b, svc) - defer tickerStop() - - b.Use(regMid.Middleware, middleware.MessageLogger, middleware2.AutoRespond()) - b.Handle(tele.OnText, msgHandler.Handle) - b.Handle(tele.OnCallback, callbackHandler.Handle) - - b.Start() -} - -func goSchedule(b *tele.Bot, svc *service.DefaultService) func() { - sch := schedulers.NewMessage(b, svc) - ticker := time.NewTicker(10 * time.Minute) - - go func() { - for { - select { - case <-ticker.C: - log.Println("SCHEDULER | Просмотр сообщений от детей ...") - sch.Schedule() - } - } - }() - - return ticker.Stop -} - -func getSqliteBase(name string) (*sql.DB, func() error) { - db, err := sql.Open("sqlite3", "file:"+name) - if err != nil { - log.Fatal(err) - } - - err = db.Ping() - if err != nil { - log.Println(err) - } - - log.Print("Подключение к базе данных установлено\n") - return db, db.Close -} diff --git a/cmd/migrations/createGroups.sql b/cmd/migrations/createGroups.sql deleted file mode 100644 index 58c2f43..0000000 --- a/cmd/migrations/createGroups.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE groups -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - group_id INTEGER NOT NULL, - owner_id INTEGER, - title TEXT NOT NULL, - time_lesson TEXT NOT NULL -); \ No newline at end of file diff --git a/cmd/migrations/createUsers.sql b/cmd/migrations/createUsers.sql deleted file mode 100644 index 7753700..0000000 --- a/cmd/migrations/createUsers.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE users -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - uid INTEGER NOT NULL UNIQUE, - user_agent TEXT DEFAULT NULL, - cookie TEXT DEFAULT NULL, - last_notification_msg TEXT DEFAULT NULL, - notification INTEGER DEFAULT 0 -); \ No newline at end of file diff --git a/go.mod b/go.mod index 70c3b7b..2024750 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,9 @@ module algobot go 1.23.3 require ( - github.com/PuerkitoBio/goquery v1.10.1 - github.com/golang/mock v1.6.0 + github.com/LZTD1/telebot-router v1.0.0 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 - github.com/joho/godotenv v1.5.1 - github.com/jxskiss/base62 v1.1.0 - github.com/ncruces/go-sqlite3 v0.22.0 github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.5 @@ -18,11 +14,9 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect - github.com/andybalholm/cascadia v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ncruces/julianday v1.0.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tetratelabs/wazero v1.8.2 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index ce77622..c56742f 100644 --- a/go.sum +++ b/go.sum @@ -60,16 +60,14 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/LZTD1/telebot-router v1.0.0 h1:Fskz8zTCfFKrrOg4LTiBgQYZXKj4nxjfVmlZRmfZzGQ= +github.com/LZTD1/telebot-router v1.0.0/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= -github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -157,7 +155,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -275,8 +272,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= -github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -322,10 +317,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/ncruces/go-sqlite3 v0.22.0 h1:FkGSBhd0TY6e66k1LVhyEpA+RnG/8QkQNed5pjIk4cs= -github.com/ncruces/go-sqlite3 v0.22.0/go.mod h1:ueXOZXYZS2OFQirCU3mHneDwJm5fGKHrtccYBeGEV7M= -github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= -github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -387,15 +378,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= -github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= @@ -433,13 +421,8 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -475,11 +458,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -527,13 +505,6 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -568,12 +539,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -652,24 +617,10 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -679,11 +630,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -744,10 +690,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index fc58fab..9a1521c 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -1,11 +1,14 @@ package telegram import ( + "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" - dispatcher2 "algobot/internal/telegram/dispatcher/text" + "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/logger" + "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" + router "github.com/LZTD1/telebot-router" tele "gopkg.in/telebot.v4" "gopkg.in/telebot.v4/middleware" "log/slog" @@ -37,24 +40,24 @@ func New(log *slog.Logger, token string) *App { os.Exit(1) } + stateMachine := memory.New() + // initialize routes b.Use(trace.New(log)) b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) - dispatcher := dispatcher2.NewDispatcher(log) - - state := memory.New() + r := router.NewRouter() - b.Handle(tele.OnText, func(c tele.Context) error { - userState := state.State(c.Sender().ID) + r.Group(func(r router.Router) { + r.Use(stater.New(stateMachine, fsm.Default)) - handler := dispatcher.GetHandlers(userState) - - return handler.Handle(c) + r.HandleFuncText("/start", text.NewStart(stateMachine)) }) + b.Handle(tele.OnText, r.ServeContext) + return &App{log: log, bot: b} } diff --git a/internal/domain/telegram/keyboards/start.go b/internal/domain/telegram/keyboards/start.go new file mode 100644 index 0000000..6f4abd4 --- /dev/null +++ b/internal/domain/telegram/keyboards/start.go @@ -0,0 +1,20 @@ +package keyboards + +import tele "gopkg.in/telebot.v4" + +func Start() *tele.ReplyMarkup { + StartKeyboard := &tele.ReplyMarkup{ResizeKeyboard: true} + + MissingBtn := StartKeyboard.Text("Получить отсутсвующих") + MyGroupsBtn := StartKeyboard.Text("Мои группы") + SettingsBtn := StartKeyboard.Text("Настройки") + AIBtn := StartKeyboard.Text("AI 🔹") + + StartKeyboard.Reply( + StartKeyboard.Row(MissingBtn), + StartKeyboard.Row(MyGroupsBtn, SettingsBtn), + StartKeyboard.Row(AIBtn), + ) + + return StartKeyboard +} diff --git a/internal/telegram/dispatcher/text/dispatcher.go b/internal/telegram/dispatcher/text/dispatcher.go deleted file mode 100644 index 83c46ba..0000000 --- a/internal/telegram/dispatcher/text/dispatcher.go +++ /dev/null @@ -1,35 +0,0 @@ -package text - -import ( - "algobot/internal/lib/fsm" - tele "gopkg.in/telebot.v4" - "log/slog" -) - -type Dispatcher struct { - log *slog.Logger - handler Handlers -} - -type Handler interface { - Handle(c tele.Context) error -} - -type Handlers map[fsm.State]Handler - -func NewDispatcher(log *slog.Logger) *Dispatcher { - - return &Dispatcher{log: log, handler: make(Handlers)} -} - -func (d *Dispatcher) Register(state fsm.State, handler Handler) { - d.handler[state] = handler -} - -func (d *Dispatcher) GetHandlers(state fsm.State) Handler { - if val, ok := d.handler[state]; ok { - return val - } - - return d.handler[fsm.Default] // TODO : refactor default val, change to ret error -} diff --git a/internal/telegram/handlers/text/chatting-ai/chattingAI.go b/internal/telegram/handlers/text/chatting-ai/chattingAI.go deleted file mode 100644 index 0afccf5..0000000 --- a/internal/telegram/handlers/text/chatting-ai/chattingAI.go +++ /dev/null @@ -1,18 +0,0 @@ -package chattingAI - -import ( - "gopkg.in/telebot.v4" - "log/slog" -) - -type ChattingAI struct { - log *slog.Logger -} - -func New(log *slog.Logger) *ChattingAI { - return &ChattingAI{} -} - -func (d ChattingAI) Handle(c telebot.Context) error { - panic("implement me") -} diff --git a/internal/telegram/handlers/text/default-state/default.go b/internal/telegram/handlers/text/default-state/default.go deleted file mode 100644 index 6e46d7c..0000000 --- a/internal/telegram/handlers/text/default-state/default.go +++ /dev/null @@ -1,20 +0,0 @@ -package defaultstate - -import ( - "gopkg.in/telebot.v4" - "log/slog" -) - -type DefaultState struct { - log *slog.Logger -} - -func New(log *slog.Logger) *DefaultState { - return &DefaultState{ - log: log, - } -} - -func (d *DefaultState) Handle(c telebot.Context) error { - panic("implement me") -} diff --git a/internal/telegram/handlers/text/sending-cookie/sendingCookie.go b/internal/telegram/handlers/text/sending-cookie/sendingCookie.go deleted file mode 100644 index 041287d..0000000 --- a/internal/telegram/handlers/text/sending-cookie/sendingCookie.go +++ /dev/null @@ -1,18 +0,0 @@ -package sendingCookie - -import ( - "gopkg.in/telebot.v4" - "log/slog" -) - -type SendingCookie struct { - log *slog.Logger -} - -func New(log *slog.Logger) *SendingCookie { - return &SendingCookie{} -} - -func (d SendingCookie) Handle(c telebot.Context) error { - panic("implement me") -} diff --git a/internal/telegram/handlers/text/start.go b/internal/telegram/handlers/text/start.go new file mode 100644 index 0000000..a1e798e --- /dev/null +++ b/internal/telegram/handlers/text/start.go @@ -0,0 +1,19 @@ +package text + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "gopkg.in/telebot.v4" +) + +type SetStater interface { + SetState(uid int64, state fsm.State) +} + +func NewStart(setStater SetStater) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + setStater.SetState(ctx.Sender().ID, fsm.Default) + + return ctx.Send("Открыто главное меню:", keyboards.Start()) + } +} diff --git a/internal/telegram/middleware/stater/stater.go b/internal/telegram/middleware/stater/stater.go new file mode 100644 index 0000000..6710dd5 --- /dev/null +++ b/internal/telegram/middleware/stater/stater.go @@ -0,0 +1,23 @@ +package stater + +import ( + "algobot/internal/lib/fsm" + router "github.com/LZTD1/telebot-router" + "gopkg.in/telebot.v4" +) + +type Stater interface { + State(uid int64) fsm.State +} + +func New(stater Stater, onState fsm.State) func(next router.RouteHandler) router.RouteHandler { + return func(next router.RouteHandler) router.RouteHandler { + return router.HandlerFunc(func(ctx telebot.Context) error { + if stater.State(ctx.Sender().ID) == onState { + return next.ServeContext(ctx) + } + + return nil + }) + } +} diff --git a/internal_old/clients/backoffice.go b/internal_old/clients/backoffice.go deleted file mode 100644 index f126dd4..0000000 --- a/internal_old/clients/backoffice.go +++ /dev/null @@ -1,305 +0,0 @@ -package clients - -import ( - appError "algobot/internal_old/error" - "encoding/json" - "errors" - "fmt" - "github.com/PuerkitoBio/goquery" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" -) - -const defaultBackofficeUrl = "https://backoffice.algoritmika.org" - -type Backoffice struct { - url string - settings BackofficeSetting -} -type BackofficeSetting struct { - Retry int - Timeout time.Duration - RetryTimeout time.Duration -} - -func NewBackoffice(url string, settings BackofficeSetting) *Backoffice { - if strings.TrimSpace(url) == "" { - url = defaultBackofficeUrl - } - return &Backoffice{url: url, settings: settings} -} - -func (b Backoffice) GetKidInfo(cookie string, kidID string) (*FullKidInfo, error) { - req, err := b.createReq("GET", "/api/v2/student/default/view/"+kidID, cookie, map[string]string{ - "expand": "groups", - }, nil) - - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidInfo(%s, %s) : %w", cookie, kidID, err) - } - data, err := b.doReq(req) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidInfo(%s, %s) : %w", cookie, kidID, err) - } - - var response FullKidInfo - err = json.NewDecoder(data.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidInfo(%s, %s) : %w", cookie, kidID, err) - } - - return &response, nil -} - -func (b Backoffice) GetGroupInfo(cookie string, group string) (*FullGroupInfo, error) { - req, err := b.createReq("GET", "/api/v1/group/"+group, cookie, map[string]string{ - "expand": "venue,teacher,curator,branch", - }, nil) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetGroupInfo(%s, %s) : %w", cookie, group, err) - } - data, err := b.doReq(req) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetGroupInfo(%s, %s) : %w", cookie, group, err) - } - - var response FullGroupInfo - err = json.NewDecoder(data.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetGroupInfo(%s, %s) : %w", cookie, group, err) - } - - return &response, nil -} - -func (b Backoffice) GetKidsNamesByGroup(cookie string, group int) (*GroupResponse, error) { - - req, err := b.createReq("GET", "/api/v2/group/student/index", cookie, map[string]string{ - "groupId": strconv.Itoa(group), - "expand": "lastGroup, groups", - }, nil) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsNamesByGroup(%s, %s) : %w", cookie, group, err) - } - - data, reqErr := b.doReq(req) - if reqErr != nil { - return nil, fmt.Errorf("Backoffice.GetKidsNamesByGroup(%s, %s) : %w", cookie, group, reqErr) - } - - var response GroupResponse - err = json.NewDecoder(data.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsNamesByGroup(%s, %s) : %w", cookie, group, err) - } - - return &response, nil -} - -func (b Backoffice) GetKidsStatsByGroup(cookie, group string) (*KidsStats, error) { - req, err := b.createReq("GET", "/api/v1/stats/default/attendance", cookie, map[string]string{ - "group": group, - }, nil) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsStatsByGroup(%s, %s) : %w", cookie, group, err) - } - - data, reqErr := b.doReq(req) - if reqErr != nil { - return nil, fmt.Errorf("Backoffice.GetKidsStatsByGroup(%s, %s) : %w", cookie, group, reqErr) - } - - var response KidsStats - err = json.NewDecoder(data.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsStatsByGroup(%s, %s) : %w", cookie, group, err) - } - - return &response, nil -} - -func (b Backoffice) GetKidsMessages(cookie string) (*KidsMessages, error) { - req, err := b.createReq("GET", "/api/v1/teacherComment/projects", cookie, map[string]string{ - "from": "0", - "limit": "30", - }, nil) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsMessages(%s) : %w", cookie, err) - } - - data, reqErr := b.doReq(req) - if reqErr != nil { - return nil, fmt.Errorf("Backoffice.GetKidsMessages(%s) : %w", cookie, reqErr) - } - - var response KidsMessages - err = json.NewDecoder(data.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetKidsMessages(%s) : %w", cookie, err) - } - - return &response, nil -} - -func (b Backoffice) GetAllGroupsByUser(cookie string) ([]AllGroupsUser, error) { - req, err := b.createReq("GET", "/group", cookie, map[string]string{ - "GroupSearch[status][]": "active", - "presetType": "all", - "_pjax": "#group-grid-pjax", - }, nil) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetAllGroupsByUser(%s) : %w", cookie, err) - } - data, reqErr := b.doReq(req) - if reqErr != nil { - return nil, fmt.Errorf("Backoffice.GetAllGroupsByUser(%s) : %w", cookie, reqErr) - } - - res, err := parsedHtml(data.Body) - if err != nil { - return nil, fmt.Errorf("Backoffice.GetAllGroupsByUser(%s) : %w", cookie, err) - } - return res, nil -} - -func (b Backoffice) OpenLession(cookie, group, lession string) error { - params := url.Values{} - params.Add("ajaxUrl", "/api/v2/group/lesson/status") - params.Add("btnClass", "btn btn-xs btn-danger") - params.Add("status", "10") - params.Add("lessonId", lession) - params.Add("groupId", group) - query := params.Encode() - - req, err := b.createReq("POST", "/api/v2/group/lesson/status", cookie, map[string]string{}, strings.NewReader(query)) - if err != nil { - return fmt.Errorf("Backoffice.OpenLession(%s, %s, %s) : %w", cookie, group, lession, err) - } - _, reqErr := b.doReq(req) - if reqErr != nil { - return fmt.Errorf("Backoffice.OpenLession(%s, %s, %s) : %w", cookie, group, lession, reqErr) - } - - return nil -} - -func (b Backoffice) CloseLession(cookie, group, lession string) error { - params := url.Values{} - params.Add("ajaxUrl", "/api/v2/group/lesson/status") - params.Add("btnClass", "btn btn-xs btn-danger") - params.Add("status", "0") - params.Add("lessonId", lession) - params.Add("groupId", group) - query := params.Encode() - - req, err := b.createReq("POST", "/api/v2/group/lesson/status", cookie, map[string]string{}, strings.NewReader(query)) - if err != nil { - return fmt.Errorf("Backoffice.CloseLession(%s, %s, %s) : %w", cookie, group, lession, err) - } - - _, reqErr := b.doReq(req) - if reqErr != nil { - return fmt.Errorf("Backoffice.CloseLession(%s, %s, %s) : %w", cookie, group, lession, reqErr) - } - - return nil -} - -func parsedHtml(body io.ReadCloser) ([]AllGroupsUser, error) { - doc, err := goquery.NewDocumentFromReader(body) - if err != nil { - return nil, fmt.Errorf("Backoffice.parsedHtml() : %w", err) - } - var groups []AllGroupsUser - - doc.Find("tr.group-grid").Each(func(i int, row *goquery.Selection) { - groupId := row.Find("td[data-col-seq='id']").First().Text() - groupId = strings.TrimSpace(groupId) - - titleCell := row.Find("td[data-col-seq='title']").First() - - groupTitle := titleCell.Find("p").First().Text() - groupTitle = strings.TrimSpace(groupTitle) - - groupTime := titleCell.Find("a").First().Text() - groupTime = strings.TrimSpace(groupTime) - - nextLessonTime := row.Find("td[data-col-seq='nextLessonTime']").First().Text() - nextLessonTime = strings.TrimSpace(nextLessonTime) - - if nextLessonTime != "" { - groups = append(groups, AllGroupsUser{ - Title: strings.ReplaceAll(groupTitle, "\u00A0", " "), - GroupId: strings.ReplaceAll(groupId, "\u00A0", " "), - TimeLesson: strings.ReplaceAll(nextLessonTime, "\u00A0", " "), - RegularTime: strings.ReplaceAll(groupTime, "\u00A0", " "), - }) - } - }) - - return groups, nil -} -func (b Backoffice) doReq(req *http.Request) (*http.Response, error) { - client := &http.Client{ - Timeout: b.settings.Timeout, - } - - var returnErr error - - for i := 0; i < b.settings.Retry; i++ { - resp, err := client.Do(req) - if err != nil { - returnErr = fmt.Errorf("Backoffice.doReq() : %w", err) - time.Sleep(b.settings.RetryTimeout) - continue - } - if resp.StatusCode >= 500 { - returnErr = fmt.Errorf("Backoffice.doReq() : %w", errors.New(resp.Status+" "+getString(resp.Body))) - time.Sleep(b.settings.RetryTimeout) - continue - } - if resp.StatusCode >= 400 && resp.StatusCode < 500 { - return nil, fmt.Errorf("Backoffice.doReq() : %w : %w", appError.ErrNotFound, getErrorByCode(resp.Status, resp.Body)) - } - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return resp, nil - } - } - return nil, returnErr -} - -func getErrorByCode(status string, body io.Reader) error { - return errors.New(status + " " + getString(body)) -} - -func getString(body io.Reader) string { - all, err := io.ReadAll(body) - if err != nil { - return "" - } - return string(all) -} -func (b Backoffice) createReq(method, uri, cookie string, params map[string]string, body io.Reader) (*http.Request, error) { - reqUrl, _ := url.Parse(fmt.Sprintf("%s%s", b.url, uri)) - p := url.Values{} - for key, val := range params { - p.Add(key, val) - } - reqUrl.RawQuery = p.Encode() - req, err := http.NewRequest(method, reqUrl.String(), body) - - if err != nil { - return nil, fmt.Errorf("Backoffice.createReq() : %w", err) - } - req.Header.Add("Cookie", cookie) - - if method == "POST" { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - } - - return req, nil -} diff --git a/internal_old/clients/webClient.go b/internal_old/clients/webClient.go deleted file mode 100644 index 3a8ccd8..0000000 --- a/internal_old/clients/webClient.go +++ /dev/null @@ -1,323 +0,0 @@ -package clients - -import ( - "fmt" - "time" -) - -type ClientError struct { - Code int - Message string -} - -func (c ClientError) Error() string { - return fmt.Sprintf("%d: %s", c.Code, c.Message) -} -func GetError(code int, message string) *ClientError { - return &ClientError{code, message} -} - -type WebClient interface { - // GetKidsNamesByGroup получить всех детей в группе - GetKidsNamesByGroup(cookie string, group int) (*GroupResponse, error) - // GetKidsStatsByGroup получить статистику посещения детей в группе - GetKidsStatsByGroup(cookie, group string) (*KidsStats, error) - // OpenLession открыть лекцию с идентификатором {lession} - OpenLession(cookie, group, lession string) error - // CloseLession закрыть лекцию с идентификатором {lession} - CloseLession(cookie, group, lession string) error - // GetKidsMessages получить новые сообщения детей на платформе - GetKidsMessages(cookie string) (*KidsMessages, error) - // GetAllGroupsByUser получить все группы - GetAllGroupsByUser(cookie string) ([]AllGroupsUser, error) - // GetGroupInfo получить информацию о группе - GetGroupInfo(cookie string, group string) (*FullGroupInfo, error) - // GetKidInfo получить информацию о ребенке - GetKidInfo(cookie string, kidID string) (*FullKidInfo, error) -} - -type GroupResponse struct { - Status string `json:"status"` - Data GroupData `json:"data"` -} -type GroupData struct { - Items []Student `json:"items"` -} -type Student struct { - ID int `json:"id"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - FullName string `json:"fullName"` - ParentName string `json:"parentName"` - Email string `json:"email"` - HasLaptop int `json:"hasLaptop"` - Phone string `json:"phone"` - Age int `json:"age"` - BirthDate time.Time `json:"birthDate"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt interface{} `json:"deletedAt"` - HasBranchAccess bool `json:"hasBranchAccess"` - Username string `json:"username"` - Password string `json:"password"` - LastGroup Group `json:"lastGroup"` - Groups []Group `json:"groups"` - Links Links `json:"_links"` -} - -type Group struct { - ID int `json:"id"` - GroupStudentID int `json:"groupStudentId"` - Title string `json:"title"` - Content string `json:"content"` - Track int `json:"track"` - Status int `json:"status"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` - CourseID int `json:"courseId"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt interface{} `json:"deletedAt"` -} - -type Links struct { - Self SelfLink `json:"self"` -} - -type SelfLink struct { - Href string `json:"href"` -} - -type KidsStats struct { - Status string `json:"status"` - Data []KidStat `json:"data"` -} - -type KidStat struct { - StudentID int `json:"student_id"` - Attendance []Attendance `json:"attendance"` -} - -type Attendance struct { - LessonID int `json:"lesson_id"` - LessonTitle string `json:"lesson_title"` - StartTimeFormatted string `json:"start_time_formatted"` - Status string `json:"status"` -} - -type KidsMessages struct { - Status string `json:"status"` - Data MessagesData `json:"data"` -} - -type MessagesData struct { - Projects []Message `json:"projects"` -} - -type Message struct { - UID string `json:"uid"` - New bool `json:"new"` - SenderID int `json:"senderId"` - SenderScope string `json:"senderScope"` - Type string `json:"type"` - Content string `json:"content"` - Name string `json:"name"` - LastTime string `json:"lastTime"` - Title string `json:"title"` - Link string `json:"link"` -} - -type AllGroupsUser struct { - Title string - GroupId string - TimeLesson string - RegularTime string -} - -// //////////////////// -type StatusFull struct { - Value int `json:"value"` - Label string `json:"label"` - Tag string `json:"tag"` -} - -type TypeFull struct { - Value string `json:"value"` - Label string `json:"label"` - Tag string `json:"tag"` -} - -type ProfileFull struct { - PhotoURL string `json:"photo_url"` - Promo string `json:"promo"` -} - -type LinksFull struct { - Self string `json:"self"` -} - -type BranchFull struct { - ID int `json:"id"` - Title string `json:"title"` - Code string `json:"code"` - Description string `json:"description"` - Phone string `json:"phone"` - Email string `json:"email"` - SiteURL string `json:"site_url"` - TemplateVersion int `json:"templateVersion"` - UseAmo bool `json:"use_amo"` - AmoConfigID int `json:"amoConfigId"` - ShowFinanceInfo bool `json:"show_finance_info"` - LmsDisplayStudentCredentials bool `json:"lms_display_student_credentials"` - ShowOnlineRoomURLField int `json:"show_online_room_url_field"` - UseSms bool `json:"use_sms"` - LanguageID int `json:"language_id"` - OrderName int `json:"order_name"` - UseFullyPaidLabel int `json:"use_fully_paid_label"` - BrandName string `json:"brandName"` - MaxCountStudentsForShowOnline int `json:"max_count_students_for_show_online"` - IsFillPaymentSystem bool `json:"isFillPaymentSystem"` - FirstLessonNoRoyalty int `json:"firstLessonNoRoyalty"` - RootBranchID int `json:"root_branch_id"` -} - -type VenueFull struct { - ID int `json:"id"` - Title string `json:"title"` - Address string `json:"address"` - ContactName string `json:"contact_name"` - ContactEmail string `json:"contact_email"` - ContactPhone string `json:"contact_phone"` - Links LinksFull `json:"_links"` -} - -type UserFull struct { - ID int `json:"id"` - Username string `json:"username"` - Phone string `json:"phone"` - Email string `json:"email"` - Name string `json:"name"` - Profile ProfileFull `json:"profile"` - Status int `json:"status"` - Links LinksFull `json:"_links"` -} - -type TeacherFull struct { - ID int `json:"id"` - Username string `json:"username"` - Phone string `json:"phone"` - Email string `json:"email"` - Name string `json:"name"` - Profile ProfileFull `json:"profile"` - AllowedUserCourses []AllowedUserCourseFull `json:"allowedUserCourses"` - Status int `json:"status"` - Links LinksFull `json:"_links"` -} - -type AllowedUserCourseFull struct { - UserID int `json:"userId"` - CourseID int `json:"courseId"` - IsAllowed int `json:"isAllowed"` -} - -type CourseTypeFull struct { - ID int `json:"id"` - Title string `json:"title"` - Code string `json:"code"` -} - -type CourseFull struct { - ID int `json:"id"` - Name string `json:"name"` - GUID string `json:"guid"` - Description string `json:"description"` - ContentType string `json:"contentType"` - CourseType CourseTypeFull `json:"courseType"` - LessonsCount int `json:"lessons_count"` - GroupLessonsAmount int `json:"group_lessons_amount"` - LessonsCountFormatted string `json:"lessons_count_formatted"` - GroupLessonsAmountFormatted string `json:"group_lessons_amount_formatted"` - IsDeleted int `json:"is_deleted"` - Links LinksFull `json:"_links"` -} - -type PriorityLevelFull struct { - Value string `json:"value"` - Label string `json:"label"` - Tag string `json:"tag"` -} - -type RelatedFull struct { - Statuses []StatusFull `json:"statuses"` - Types []TypeFull `json:"types"` - PriorityLevels []PriorityLevelFull `json:"priority_levels"` -} - -type FullGroupInfo struct { - Status string `json:"status"` - Data GroupDataFull `json:"data"` -} - -type GroupDataFull struct { - ID int `json:"id"` - Title string `json:"title"` - Content string `json:"content"` - Type TypeFull `json:"type"` - Status StatusFull `json:"status"` - StatusChangedAt string `json:"status_changed_at"` - StartTime string `json:"start_time"` - NextLessonTime string `json:"next_lesson_time"` - LessonsTotal int `json:"lessons_total"` - LessonsPassed int `json:"lessons_passed"` - HardwareNeeded int `json:"hardware_needed"` - Branch BranchFull `json:"branch"` - Venue VenueFull `json:"venue"` - Curator UserFull `json:"curator"` - Teacher TeacherFull `json:"teacher"` - Teachers []TeacherFull `json:"teachers"` - ClientManager interface{} `json:"client_manager"` - Course CourseFull `json:"course"` - LanguageID interface{} `json:"language_id"` - Journal bool `json:"journal"` - ShowJournal bool `json:"show_journal"` - ShowOnlineRoom bool `json:"showOnlineRoom"` - IsOnline bool `json:"isOnline"` - ActiveStudentCount int `json:"active_student_count"` - OnlineRoomURL string `json:"online_room_url"` - UseClientManager int `json:"use_client_manager"` - DisplayLessonDurationInMinutes int `json:"display_lesson_duration_in_minutes"` - DeletedAt interface{} `json:"deleted_at"` - DeletedBy interface{} `json:"deleted_by"` - PriorityLevel PriorityLevelFull `json:"priority_level"` - IsFull bool `json:"is_full"` - CreatedAt string `json:"created_at"` - CreatedBy UserFull `json:"created_by"` - Related RelatedFull `json:"_related"` -} - -// FullKidInfo -type LinksKidInfo struct { - Self struct { - Href string `json:"href"` - } `json:"self"` -} - -type GroupKidInfo struct { - ID int `json:"id"` - GroupStudentID int `json:"groupStudentId"` - Title string `json:"title"` - Content string `json:"content"` - Track int `json:"track"` - Status int `json:"status"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` - CourseID int `json:"courseId"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt any `json:"deletedAt"` -} - -type FullKidInfo struct { - Status string `json:"status"` - Data Student `json:"data"` -} diff --git a/internal_old/config/keyboards.go b/internal_old/config/keyboards.go deleted file mode 100644 index 255a9db..0000000 --- a/internal_old/config/keyboards.go +++ /dev/null @@ -1,50 +0,0 @@ -package config - -import tele "gopkg.in/telebot.v4" - -var ( - StartKeyboard = &tele.ReplyMarkup{ResizeKeyboard: true} - MissingBtn = StartKeyboard.Text("Получить отсутсвующих") - MyGroupsBtn = StartKeyboard.Text("Мои группы") - SettingsBtn = StartKeyboard.Text("Настройки") - AIBtn = StartKeyboard.Text("AI 🔹") - - MyGroupsKeyboard = &tele.ReplyMarkup{ResizeKeyboard: true} - refreshGroupsBtn = MyGroupsKeyboard.Data("Обновить группы", "refresh_groups") - - SettingsKeyboard = &tele.ReplyMarkup{ResizeKeyboard: true} - SetCookieBtn = SettingsKeyboard.Data("Установить Cookie", "set_cookie") - ChangeNotificationBtn = SettingsKeyboard.Data("Переключить уведомления", "change_notification") - - RejectKeyboard = &tele.ReplyMarkup{ResizeKeyboard: true} - RejectActionBtn = RejectKeyboard.Text("Отменить действие") - - AIKeyboard = &tele.ReplyMarkup{ResizeKeyboard: true} - BackBtn = StartKeyboard.Text("⬅️ Назад") - ClearHistoryBtn = StartKeyboard.Text("Отчистить чат") -) - -func init() { - StartKeyboard.Reply( - StartKeyboard.Row(MissingBtn), - StartKeyboard.Row(MyGroupsBtn, SettingsBtn), - StartKeyboard.Row(AIBtn), - ) - - MyGroupsKeyboard.Inline( - MyGroupsKeyboard.Row(refreshGroupsBtn), - ) - - SettingsKeyboard.Inline( - SettingsKeyboard.Row(SetCookieBtn), - SettingsKeyboard.Row(ChangeNotificationBtn), - ) - - RejectKeyboard.Reply( - RejectKeyboard.Row(RejectActionBtn), - ) - - AIKeyboard.Reply( - RejectKeyboard.Row(BackBtn, ClearHistoryBtn), - ) -} diff --git a/internal_old/config/texts.go b/internal_old/config/texts.go deleted file mode 100644 index 38e6a14..0000000 --- a/internal_old/config/texts.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -const SetParam = "✅" -const NotSetParam = "❌" - -const HelloWorld = "Здравствуйте, вы успешно зарегистрировались в боте, для продолжения, воспользуйтесь кнопками на клавиатуре" -const StartText = "Открыто главное меню:" -const Incorrect = "Упс, такие команды я не понимаю" -const Settings = "🔧 Ваши настройки:" -const Cookie = "Куки:" -const ChatNotifications = "Уведомление от чата:" - -const CookieNotSetException = "Сперва вам необходимо установить куки!" - -const GroupName = "Группа по курсу: " -const Lection = "Лекция: " -const TotalKids = "Общее число детей: " -const MissingKids = "Отсутствуют: " - -const MyGroups = "Всего групп: " -const UserDontHaveGroup = "У вас нету никаких групп!" -const CurrentGroupDontFind = "В данный момент, никакой группы не найдено!\nПопробуйте обновить свои группы" - -const SendingCookie = "Отправьте мне свои cookie 🍪\nИнструкция: https://telegra.ph/Kak-dobavit-v-bota-svoi-Cookie-02-05" -const CookieSet = "Куки успешно установлены" - -const UpdateStarted = "Происходит процесс обновления ..." -const UpdateEnd = "Успешно обновлено!" - -const OpenLessonBtn = "Открыть лекцию" -const CloseLessonBtn = "Закрыть лекцию" -const GetCredsBtn = "Получить аккаунты" - -const SuccessfulChangeStatus = "Статус лекции успешно изменен" diff --git a/internal_old/contextHandlers/callbackHandlers/changeNotification.go b/internal_old/contextHandlers/callbackHandlers/changeNotification.go deleted file mode 100644 index f4039d7..0000000 --- a/internal_old/contextHandlers/callbackHandlers/changeNotification.go +++ /dev/null @@ -1,39 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/contextHandlers/textHandlers/defaultState" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "gopkg.in/telebot.v4" -) - -type ChangeNotification struct { - svc service.Service - settings defaultHandler.ContextHandler -} - -func NewChangeNotification(svc service.Service) *ChangeNotification { - return &ChangeNotification{svc: svc, settings: defaultState.NewSettings(svc)} -} - -func (c ChangeNotification) CanHandle(ctx telebot.Context) bool { - if ctx.Callback().Data == "change_notification" { - return true - } - return false -} - -func (c ChangeNotification) Process(ctx telebot.Context) error { - uid := ctx.Callback().Sender.ID - notify, err := c.svc.Notification(uid) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при получении нотификаций!") - } - err = c.svc.SetNotification(uid, !notify) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при установлении нотификаций!") - } - - return ctx.Edit("Настройки уведомлений были изменены!") -} diff --git a/internal_old/contextHandlers/callbackHandlers/closeLesson.go b/internal_old/contextHandlers/callbackHandlers/closeLesson.go deleted file mode 100644 index 305dcdb..0000000 --- a/internal_old/contextHandlers/callbackHandlers/closeLesson.go +++ /dev/null @@ -1,48 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "fmt" - "gopkg.in/telebot.v4" - "strconv" - "strings" -) - -type CloseLesson struct { - s service.Service -} - -func NewCloseLesson(s service.Service) *CloseLesson { - return &CloseLesson{s: s} -} - -func (c CloseLesson) CanHandle(ctx telebot.Context) bool { - if strings.HasPrefix(ctx.Callback().Data, "close_lesson_") { - return true - } - return false -} - -func (c CloseLesson) Process(ctx telebot.Context) error { - data := strings.Split(strings.TrimPrefix(ctx.Callback().Data, "close_lesson_"), "_") - if len(data) != 2 { - return helpers.LogError(fmt.Errorf("%s : %w", ctx.Callback().Data, appError.NotEnoughArgs), ctx, "(1) Ошибка при анализе данных от кнопки!") - } - groupID, err := strconv.Atoi(data[0]) - if err != nil { - return helpers.LogError(err, ctx, "(2) Ошибка при анализе данных от кнопки!") - } - lessionID, err := strconv.Atoi(data[1]) - if err != nil { - return helpers.LogError(err, ctx, "(3) Ошибка при анализе данных от кнопки!") - } - err = c.s.CloseLesson(ctx.Callback().Sender.ID, groupID, lessionID) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при закрытии лекции") - } - - return ctx.Send(config.SuccessfulChangeStatus) -} diff --git a/internal_old/contextHandlers/callbackHandlers/getCredentials.go b/internal_old/contextHandlers/callbackHandlers/getCredentials.go deleted file mode 100644 index d86bba6..0000000 --- a/internal_old/contextHandlers/callbackHandlers/getCredentials.go +++ /dev/null @@ -1,50 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "fmt" - "gopkg.in/telebot.v4" - "strconv" - "strings" -) - -type GetCredentials struct { - s service.Service -} - -func NewGetCredentials(s service.Service) *GetCredentials { - return &GetCredentials{s: s} -} - -func (c GetCredentials) CanHandle(ctx telebot.Context) bool { - if strings.HasPrefix(ctx.Callback().Data, "get_creds_") { - return true - } - return false -} - -func (c GetCredentials) Process(ctx telebot.Context) error { - data := strings.TrimPrefix(ctx.Callback().Data, "get_creds_") - - groupID, err := strconv.Atoi(data) - if err != nil { - return helpers.LogError(err, ctx, "(1) Ошибка при анализе данных от кнопки!") - } - - creds, err := c.s.AllCredentials(ctx.Callback().Sender.ID, groupID) - if err != nil { - return helpers.LogError(err, ctx, "(2) Ошибка при анализе данных от кнопки!") - } - - return ctx.Send(getCreds(creds)) -} - -func getCreds(creds map[string]string) string { - sb := strings.Builder{} - sb.WriteString("Учетные записи детей:\n") - for key, val := range creds { - sb.WriteString(fmt.Sprintf("\n%v [%v]", key, val)) - } - return sb.String() -} diff --git a/internal_old/contextHandlers/callbackHandlers/openLesson.go b/internal_old/contextHandlers/callbackHandlers/openLesson.go deleted file mode 100644 index 8a94f40..0000000 --- a/internal_old/contextHandlers/callbackHandlers/openLesson.go +++ /dev/null @@ -1,48 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "fmt" - "gopkg.in/telebot.v4" - "strconv" - "strings" -) - -type OpenLesson struct { - s service.Service -} - -func NewOpenLesson(s service.Service) *OpenLesson { - return &OpenLesson{s: s} -} - -func (c OpenLesson) CanHandle(ctx telebot.Context) bool { - if strings.HasPrefix(ctx.Callback().Data, "open_lesson_") { - return true - } - return false -} - -func (c OpenLesson) Process(ctx telebot.Context) error { - data := strings.Split(strings.TrimPrefix(ctx.Callback().Data, "open_lesson_"), "_") - if len(data) != 2 { - return helpers.LogError(fmt.Errorf("%s : %w", ctx.Callback().Data, appError.NotEnoughArgs), ctx, "(1) Ошибка при анализе данных от кнопки!") - } - groupID, err := strconv.Atoi(data[0]) - if err != nil { - return helpers.LogError(err, ctx, "(2) Ошибка при анализе данных от кнопки!") - } - lessionID, err := strconv.Atoi(data[1]) - if err != nil { - return helpers.LogError(err, ctx, "(3) Ошибка при анализе данных от кнопки!") - } - err = c.s.OpenLesson(ctx.Callback().Sender.ID, groupID, lessionID) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при открытии лекции") - } - - return ctx.Send(config.SuccessfulChangeStatus) -} diff --git a/internal_old/contextHandlers/callbackHandlers/refreshGroups.go b/internal_old/contextHandlers/callbackHandlers/refreshGroups.go deleted file mode 100644 index 726097d..0000000 --- a/internal_old/contextHandlers/callbackHandlers/refreshGroups.go +++ /dev/null @@ -1,42 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "errors" - "gopkg.in/telebot.v4" -) - -type RefreshGroups struct { - svc service.Service -} - -func NewRefreshGroups(svc service.Service) *RefreshGroups { - return &RefreshGroups{svc: svc} -} - -func (r RefreshGroups) CanHandle(ctx telebot.Context) bool { - if ctx.Callback().Data == "refresh_groups" { - return true - } - return false -} - -func (r RefreshGroups) Process(ctx telebot.Context) error { - err := ctx.Edit(config.UpdateStarted) - if err != nil { - return err - } - - err = r.svc.RefreshGroups(ctx.Sender().ID) - if err != nil { - if errors.Is(err, appError.ErrHasNone) { - return ctx.Edit("Я не смог найти ни одной группы, может быть дело в cookie?") - } - return helpers.LogError(err, ctx, "Ошибка при обновлении групп!") - } - - return ctx.Edit(config.UpdateEnd) -} diff --git a/internal_old/contextHandlers/callbackHandlers/setCookie.go b/internal_old/contextHandlers/callbackHandlers/setCookie.go deleted file mode 100644 index 9b87350..0000000 --- a/internal_old/contextHandlers/callbackHandlers/setCookie.go +++ /dev/null @@ -1,31 +0,0 @@ -package callbackHandlers - -import ( - "algobot/internal_old/config" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "gopkg.in/telebot.v4" -) - -type SetCookie struct { - svc service.Service - state stateMachine.StateMachine -} - -func NewSetCookie(svc service.Service, state stateMachine.StateMachine) *SetCookie { - return &SetCookie{svc: svc, state: state} -} - -func (s *SetCookie) CanHandle(ctx telebot.Context) bool { - if ctx.Callback().Data == "set_cookie" { - return true - } - - return false -} - -func (s *SetCookie) Process(ctx telebot.Context) error { - s.state.SetStatement(ctx.Callback().Sender.ID, stateMachine.SendingCookie) - - return ctx.Send(config.SendingCookie, config.RejectKeyboard) -} diff --git a/internal_old/contextHandlers/defaultHandler/handler.go b/internal_old/contextHandlers/defaultHandler/handler.go deleted file mode 100644 index 81a57a0..0000000 --- a/internal_old/contextHandlers/defaultHandler/handler.go +++ /dev/null @@ -1,8 +0,0 @@ -package defaultHandler - -import "gopkg.in/telebot.v4" - -type ContextHandler interface { - CanHandle(ctx telebot.Context) bool - Process(ctx telebot.Context) error -} diff --git a/internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go b/internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go deleted file mode 100644 index ad44719..0000000 --- a/internal_old/contextHandlers/handlersHolders/DefaultCBHolder.go +++ /dev/null @@ -1,32 +0,0 @@ -package handlersHolders - -import ( - "algobot/internal_old/contextHandlers/callbackHandlers" - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" -) - -type DefaultCBHolder struct { - service service.Service - state stateMachine.StateMachine -} - -func NewDefaultCBHolder(service service.Service, state stateMachine.StateMachine) *DefaultCBHolder { - return &DefaultCBHolder{service: service, state: state} -} - -func (d DefaultCBHolder) HolderType() stateMachine.Statement { - return stateMachine.Default -} - -func (d DefaultCBHolder) GetHandlers() []defaultHandler.ContextHandler { - return []defaultHandler.ContextHandler{ - callbackHandlers.NewSetCookie(d.service, d.state), - callbackHandlers.NewChangeNotification(d.service), - callbackHandlers.NewRefreshGroups(d.service), - callbackHandlers.NewCloseLesson(d.service), - callbackHandlers.NewOpenLesson(d.service), - callbackHandlers.NewGetCredentials(d.service), - } -} diff --git a/internal_old/contextHandlers/handlersHolders/DefaultHolder.go b/internal_old/contextHandlers/handlersHolders/DefaultHolder.go deleted file mode 100644 index f0920c0..0000000 --- a/internal_old/contextHandlers/handlersHolders/DefaultHolder.go +++ /dev/null @@ -1,33 +0,0 @@ -package handlersHolders - -import ( - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/contextHandlers/textHandlers/defaultState" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" -) - -type DefaultHolders struct { - service service.Service - state stateMachine.StateMachine -} - -func NewDefaultHolders(service service.Service, state stateMachine.StateMachine) *DefaultHolders { - return &DefaultHolders{service: service, state: state} -} - -func (d DefaultHolders) HolderType() stateMachine.Statement { - return stateMachine.Default -} - -func (d DefaultHolders) GetHandlers() []defaultHandler.ContextHandler { - return []defaultHandler.ContextHandler{ - &defaultState.Start{}, - defaultState.NewMissingKids(d.service), - defaultState.NewSettings(d.service), - defaultState.NewMyGroups(d.service), - defaultState.NewAbsentKids(d.service), - defaultState.NewStartWithPayload(d.service), - defaultState.NewAIChat(d.service, d.state), - } -} diff --git a/internal_old/contextHandlers/handlersHolders/SendingCookie.go b/internal_old/contextHandlers/handlersHolders/SendingCookie.go deleted file mode 100644 index f063d8f..0000000 --- a/internal_old/contextHandlers/handlersHolders/SendingCookie.go +++ /dev/null @@ -1,28 +0,0 @@ -package handlersHolders - -import ( - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/contextHandlers/textHandlers/sendingCookieState" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" -) - -type SendingCookie struct { - service service.Service - state stateMachine.StateMachine -} - -func NewSendingCookie(service service.Service, state stateMachine.StateMachine) *SendingCookie { - return &SendingCookie{service: service, state: state} -} - -func (s SendingCookie) HolderType() stateMachine.Statement { - return stateMachine.SendingCookie -} - -func (s SendingCookie) GetHandlers() []defaultHandler.ContextHandler { - return []defaultHandler.ContextHandler{ - sendingCookieState.NewRejectAction(s.state), - sendingCookieState.NewSendingCookieAction(s.state, s.service), - } -} diff --git a/internal_old/contextHandlers/handlersHolders/chattingAI.go b/internal_old/contextHandlers/handlersHolders/chattingAI.go deleted file mode 100644 index 851b19f..0000000 --- a/internal_old/contextHandlers/handlersHolders/chattingAI.go +++ /dev/null @@ -1,34 +0,0 @@ -package handlersHolders - -import ( - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/contextHandlers/textHandlers/chattingAi" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" -) - -type ChattingAi struct { - state stateMachine.StateMachine - service service.Service - ai service.AIService -} - -func NewChattingAi(service service.Service, state stateMachine.StateMachine, ai service.AIService) *ChattingAi { - return &ChattingAi{ - service: service, - state: state, - ai: ai, - } -} - -func (c *ChattingAi) HolderType() stateMachine.Statement { - return stateMachine.ChattingAI -} - -func (c *ChattingAi) GetHandlers() []defaultHandler.ContextHandler { - return []defaultHandler.ContextHandler{ - chattingAi.NewBackAction(c.state), - chattingAi.NewClearHistory(c.ai), - chattingAi.NewAnyMessage(c.ai), - } -} diff --git a/internal_old/contextHandlers/handlersHolders/holder.go b/internal_old/contextHandlers/handlersHolders/holder.go deleted file mode 100644 index f9b0481..0000000 --- a/internal_old/contextHandlers/handlersHolders/holder.go +++ /dev/null @@ -1,11 +0,0 @@ -package handlersHolders - -import ( - "algobot/internal_old/contextHandlers/defaultHandler" - "algobot/internal_old/stateMachine" -) - -type HandlersHolder interface { - HolderType() stateMachine.Statement - GetHandlers() []defaultHandler.ContextHandler -} diff --git a/internal_old/contextHandlers/onCallback.go b/internal_old/contextHandlers/onCallback.go deleted file mode 100644 index ed5352c..0000000 --- a/internal_old/contextHandlers/onCallback.go +++ /dev/null @@ -1,58 +0,0 @@ -package contextHandlers - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers/handlersHolders" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "errors" - "fmt" - "gopkg.in/telebot.v4" - "strings" -) - -type OnCallback struct { - holders []handlersHolders.HandlersHolder - state stateMachine.StateMachine -} - -func NewOnCallback(service service.Service, state stateMachine.StateMachine) *OnCallback { - h := []handlersHolders.HandlersHolder{ - handlersHolders.NewDefaultCBHolder(service, state), - } - - return &OnCallback{holders: h, state: state} -} - -func (h *OnCallback) Handle(ctx telebot.Context) error { - st := h.state.GetStatement(ctx.Sender().ID) - holder := h.getHolder(st) - - if holder != nil { - - if strings.HasPrefix(ctx.Callback().Data, "\f") { - ctx.Callback().Data = strings.TrimPrefix(ctx.Callback().Data, "\f") - } - - for _, hand := range holder.GetHandlers() { - if hand.CanHandle(ctx) { - return hand.Process(ctx) - } - } - h.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return ctx.Send(config.Incorrect, config.StartKeyboard) - } - - h.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return helpers.LogError(errors.New(fmt.Sprintf("HolderNotFound(%v,%v)", st, ctx)), ctx, "Ошибка обработки запроса!\nНажмите - /start , для возвращения в меню") -} - -func (h *OnCallback) getHolder(st stateMachine.Statement) handlersHolders.HandlersHolder { - for _, holder := range h.holders { - if holder.HolderType() == st { - return holder - } - } - return nil -} diff --git a/internal_old/contextHandlers/onText.go b/internal_old/contextHandlers/onText.go deleted file mode 100644 index 4a0c545..0000000 --- a/internal_old/contextHandlers/onText.go +++ /dev/null @@ -1,56 +0,0 @@ -package contextHandlers - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers/handlersHolders" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "errors" - "fmt" - "gopkg.in/telebot.v4" -) - -type OnText struct { - holders []handlersHolders.HandlersHolder - state stateMachine.StateMachine - ai service.AIService -} - -func NewOnText(service service.Service, state stateMachine.StateMachine, ai service.AIService) *OnText { - - h := []handlersHolders.HandlersHolder{ - handlersHolders.NewDefaultHolders(service, state), - handlersHolders.NewSendingCookie(service, state), - handlersHolders.NewChattingAi(service, state, ai), - } - - return &OnText{holders: h, state: state, ai: ai} -} - -func (m *OnText) Handle(ctx telebot.Context) error { - st := m.state.GetStatement(ctx.Sender().ID) - - holder := m.getHolder(st) - if holder != nil { - for _, h := range holder.GetHandlers() { - if h.CanHandle(ctx) { - return h.Process(ctx) - } - } - m.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return ctx.Send(config.Incorrect, config.StartKeyboard) - } - - m.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return helpers.LogError(errors.New(fmt.Sprintf("HolderNotFound(%v,%v)", st, ctx)), ctx, "Ошибка обработки запроса!\nНажмите - /start , для возвращения в меню") -} - -func (m *OnText) getHolder(st stateMachine.Statement) handlersHolders.HandlersHolder { - for _, holder := range m.holders { - if holder.HolderType() == st { - return holder - } - } - return nil -} diff --git a/internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go b/internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go deleted file mode 100644 index 5ee97c9..0000000 --- a/internal_old/contextHandlers/textHandlers/chattingAi/AnyMessage.go +++ /dev/null @@ -1,37 +0,0 @@ -package chattingAi - -import ( - "algobot/internal_old/config" - "algobot/internal_old/helpers" - "algobot/internal_old/schedulers" - "algobot/internal_old/service" - "gopkg.in/telebot.v4" - "strconv" -) - -type AnyMessage struct { - s service.AIService -} - -func NewAnyMessage(s service.AIService) *AnyMessage { - return &AnyMessage{s: s} -} - -func (a AnyMessage) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text != config.BackBtn.Text && ctx.Message().Text != config.ClearHistoryBtn.Text { - return true - } - return false -} - -func (a AnyMessage) Process(ctx telebot.Context) error { - m, _ := ctx.Bot().Send(schedulers.RecipientUser{ - ID: strconv.FormatInt(ctx.Sender().ID, 10), - }, "⚙️ Думаю как ответить ....") - suggest, err := a.s.GetSuggestion(ctx.Sender().ID, ctx.Message().Text) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при получении ответа от бота!") - } - _, err = ctx.Bot().Edit(m, suggest, telebot.ModeMarkdown) - return err -} diff --git a/internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go b/internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go deleted file mode 100644 index 7dd7724..0000000 --- a/internal_old/contextHandlers/textHandlers/chattingAi/ClearHistory.go +++ /dev/null @@ -1,32 +0,0 @@ -package chattingAi - -import ( - "algobot/internal_old/config" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "gopkg.in/telebot.v4" -) - -type ClearHistory struct { - ai service.AIService -} - -func NewClearHistory(ai service.AIService) *ClearHistory { - return &ClearHistory{ai: ai} -} - -func (c ClearHistory) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == config.ClearHistoryBtn.Text { - return true - } - return false -} - -func (c ClearHistory) Process(ctx telebot.Context) error { - err := c.ai.ClearAllHistory(ctx.Sender().ID) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при отчистке памяти!") - } - - return ctx.Send("Успешно отчищено!") -} diff --git a/internal_old/contextHandlers/textHandlers/chattingAi/backAction.go b/internal_old/contextHandlers/textHandlers/chattingAi/backAction.go deleted file mode 100644 index 765d1c0..0000000 --- a/internal_old/contextHandlers/textHandlers/chattingAi/backAction.go +++ /dev/null @@ -1,27 +0,0 @@ -package chattingAi - -import ( - "algobot/internal_old/config" - "algobot/internal_old/stateMachine" - "gopkg.in/telebot.v4" -) - -type BackAction struct { - state stateMachine.StateMachine -} - -func NewBackAction(s stateMachine.StateMachine) *BackAction { - return &BackAction{state: s} -} - -func (b *BackAction) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == config.BackBtn.Text { - return true - } - return false -} - -func (b *BackAction) Process(ctx telebot.Context) error { - b.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return ctx.Send(config.StartText, config.StartKeyboard) -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/absentKids.go b/internal_old/contextHandlers/textHandlers/defaultState/absentKids.go deleted file mode 100644 index 5d224f0..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/absentKids.go +++ /dev/null @@ -1,66 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "errors" - "gopkg.in/telebot.v4" - "strings" - "time" -) - -type AbsentKids struct { - s service.Service -} - -func NewAbsentKids(s service.Service) *AbsentKids { - return &AbsentKids{s: s} -} - -func (a AbsentKids) CanHandle(ctx telebot.Context) bool { - if strings.HasPrefix(ctx.Message().Text, "/abs") { - return true - } - return false -} - -func (a AbsentKids) Process(ctx telebot.Context) error { - if ctx.Message().Payload == "" { - return ctx.Send("Формат сообщения - '/abs 2025-01-12 15:32'\nВыдаст статистику за 2025г. 12 Января, 15ч 32м") - } - t, err := time.Parse("2006-01-02 15:04", ctx.Message().Payload) - if err != nil { - return ctx.Send("Формат сообщения - '/abs 2025-01-12 15:32'\nВыдаст статистику за 2025г. 12 Января, 15ч 32м") - } - uid := ctx.Message().Sender.ID - - g, e := a.s.CurrentGroup(uid, t) - if e != nil { - if errors.Is(e, appError.ErrHasNone) { - return ctx.Send(config.CurrentGroupDontFind) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке получить текущую группу") - } - actual, err := a.s.ActualInformation(uid, t, g.GroupID) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return ctx.Send(config.CookieNotSetException) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке подгрузить информацию о группе") - } - - allKids, err := a.s.AllKidsNames(uid, g.GroupID) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return ctx.Send(config.CookieNotSetException) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке подгрузить имена детей") - } - - return ctx.Send(msgMissingKids(g, actual, allKids), getMissingKidsKeyboard(g, actual), telebot.ModeMarkdown) -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/aiChat.go b/internal_old/contextHandlers/textHandlers/defaultState/aiChat.go deleted file mode 100644 index 86a232d..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/aiChat.go +++ /dev/null @@ -1,29 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "gopkg.in/telebot.v4" -) - -type AIChat struct { - svc service.Service - state stateMachine.StateMachine -} - -func NewAIChat(s service.Service, state stateMachine.StateMachine) *AIChat { - return &AIChat{svc: s, state: state} -} - -func (a *AIChat) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == config.AIBtn.Text { - return true - } - return false -} - -func (a *AIChat) Process(ctx telebot.Context) error { - a.state.SetStatement(ctx.Sender().ID, stateMachine.ChattingAI) - return ctx.Send("Привет! Используй чат и клавиатуру для общения со мной!", config.AIKeyboard) -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/missingKids.go b/internal_old/contextHandlers/textHandlers/defaultState/missingKids.go deleted file mode 100644 index d0c1423..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/missingKids.go +++ /dev/null @@ -1,98 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/models" - "algobot/internal_old/service" - "errors" - "fmt" - "gopkg.in/telebot.v4" - "strings" -) - -type MissingKids struct { - s service.Service -} - -func NewMissingKids(s service.Service) *MissingKids { - return &MissingKids{s} -} - -func (m *MissingKids) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == "Получить отсутсвующих" { - return true - } - return false -} - -func (m *MissingKids) Process(ctx telebot.Context) error { - t := ctx.Message().Time() - uid := ctx.Message().Sender.ID - - g, e := m.s.CurrentGroup(uid, t) - if e != nil { - if errors.Is(e, appError.ErrHasNone) { - return ctx.Send(config.CurrentGroupDontFind) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке получить текущую группу") - } - - actual, err := m.s.ActualInformation(uid, t, g.GroupID) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return ctx.Send(config.CookieNotSetException) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке подгрузить информацию о группе") - } - - allKids, err := m.s.AllKidsNames(uid, g.GroupID) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return ctx.Send(config.CookieNotSetException) - } - - return helpers.LogError(e, ctx, "Произошла непредвиденная ошибка при попытке подгрузить имена детей") - } - - return ctx.Send(msgMissingKids(g, actual, allKids), getMissingKidsKeyboard(g, actual), telebot.ModeMarkdown) -} - -func getMissingKidsKeyboard(g models.Group, actual models.ActualInformation) *telebot.ReplyMarkup { - markup := telebot.ReplyMarkup{ResizeKeyboard: true} - markup.Inline( - markup.Row(markup.Data(config.CloseLessonBtn, fmt.Sprintf("close_lesson_%d_%d", g.GroupID, actual.LessonId)), markup.Data(config.OpenLessonBtn, fmt.Sprintf("open_lesson_%d_%d", g.GroupID, actual.LessonId))), - markup.Row(markup.Data(config.GetCredsBtn, fmt.Sprintf("get_creds_%d", g.GroupID))), - ) - - return &markup -} - -func msgMissingKids(g models.Group, actual models.ActualInformation, kids models.AllKids) string { - miss := strings.Builder{} - miss.WriteString("\n```Отсутствующие\n") - missingCount := 0 - for _, kid := range actual.MissingKids { - if v, ok := kids[kid.Id]; ok == true { - missingCount++ - miss.WriteString(fmt.Sprintf("%s", v.FullName)) - if kid.Count > 1 { - miss.WriteString(fmt.Sprintf(" (Уже %d занятие)", kid.Count)) - } - miss.WriteString("\n") - } - } - miss.WriteString("```") - - sb := strings.Builder{} - sb.WriteString(fmt.Sprintf("%s%s", config.GroupName, g.Title)) - sb.WriteString(fmt.Sprintf("\n%s%s\n", config.Lection, actual.LessonTitle)) - sb.WriteString(fmt.Sprintf("\n%s%d", config.TotalKids, len(kids))) - sb.WriteString(fmt.Sprintf("\n%s%d\n", config.MissingKids, missingCount)) - sb.WriteString(miss.String()) - - return sb.String() -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/myGroups.go b/internal_old/contextHandlers/textHandlers/defaultState/myGroups.go deleted file mode 100644 index ead6853..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/myGroups.go +++ /dev/null @@ -1,88 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/models" - "algobot/internal_old/serdes" - "algobot/internal_old/service" - "errors" - "fmt" - "gopkg.in/telebot.v4" - "os" - "strconv" - "strings" - "time" -) - -var locales = map[time.Weekday]string{ - time.Monday: "пн", - time.Tuesday: "вт", - time.Wednesday: "ср", - time.Thursday: "чт", - time.Friday: "пт", - time.Saturday: "сб", - time.Sunday: "вс", -} - -type MyGroups struct { - s service.Service -} - -func NewMyGroups(s service.Service) *MyGroups { - return &MyGroups{s} -} - -func (m MyGroups) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == "Мои группы" { - return true - } - return false -} -func (m MyGroups) Process(ctx telebot.Context) error { - g, e := m.s.Groups(ctx.Message().Sender.ID) - - if e != nil { - if errors.Is(e, appError.ErrHasNone) { - return ctx.Send(config.UserDontHaveGroup, config.MyGroupsKeyboard) - } - return helpers.LogError(e, ctx, "Ошибка при попытке получить группы!") - } - sorted := helpers.GetSortedGroups(g) - - return ctx.Send(GetMyGroupsMessage(sorted), config.MyGroupsKeyboard, telebot.ModeMarkdown, telebot.NoPreview) -} - -func GetMyGroupsMessage(g []models.Group) string { - s := strings.Builder{} - s.WriteString(fmt.Sprintf("%s%d\n", config.MyGroups, len(g))) - - before := g[0].TimeLesson.Weekday() - c := 1 - for _, group := range g { - if before != group.TimeLesson.Weekday() { - c = 1 - before = group.TimeLesson.Weekday() - s.WriteString("\n") - } - s.WriteString("\n") - s.WriteString(fmt.Sprintf("%d. %s 🕐 %s %s", c, getGroupTitle(group), getLocale(group.TimeLesson), group.TimeLesson.Format("15:04"))) - c += 1 - } - - return s.String() -} - -func getGroupTitle(group models.Group) string { - ser := serdes.Serialize(models.StartPayload{ - Action: models.GetGroupInfo, - Payload: []string{strconv.Itoa(group.GroupID)}, - }) - - return fmt.Sprintf("[%s](t.me/%s?start=%s)", group.Title, os.Getenv("TELEGRAM_NAME"), ser) -} - -func getLocale(t time.Time) string { - return locales[t.Weekday()] -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/settings.go b/internal_old/contextHandlers/textHandlers/defaultState/settings.go deleted file mode 100644 index 93b8f75..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/settings.go +++ /dev/null @@ -1,59 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "gopkg.in/telebot.v4" - "strings" -) - -type Settings struct { - svc service.Service -} - -func NewSettings(svc service.Service) *Settings { - return &Settings{svc: svc} -} - -func (s *Settings) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == "Настройки" { - return true - } - return false -} -func (s *Settings) Process(ctx telebot.Context) error { - uid := ctx.Message().Sender.ID - - c, err := s.svc.Cookie(uid) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при формировании настроек (получение cookie) !") - } - n, err := s.svc.Notification(uid) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при формировании настроек (получение нотификаций) !") - } - - return ctx.Send(GetMessageSettings(c, n), config.SettingsKeyboard) -} - -func GetMessageSettings(c string, n bool) string { - msg := strings.Builder{} - msg.WriteString(config.Settings) - msg.WriteString("\n\n") - msg.WriteString(config.Cookie) - if c != "" { - msg.WriteString(config.SetParam) - } else { - msg.WriteString(config.NotSetParam) - } - msg.WriteString("\n") - msg.WriteString(config.ChatNotifications) - if n { - msg.WriteString(config.SetParam) - } else { - msg.WriteString(config.NotSetParam) - } - - return msg.String() -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/start.go b/internal_old/contextHandlers/textHandlers/defaultState/start.go deleted file mode 100644 index ff77c1f..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/start.go +++ /dev/null @@ -1,19 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/config" - "gopkg.in/telebot.v4" -) - -type Start struct { -} - -func (h *Start) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == "/start" { - return true - } - return false -} -func (h *Start) Process(ctx telebot.Context) error { - return ctx.Send(config.StartText, config.StartKeyboard) -} diff --git a/internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go b/internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go deleted file mode 100644 index 72acf4c..0000000 --- a/internal_old/contextHandlers/textHandlers/defaultState/startWithPayload.go +++ /dev/null @@ -1,140 +0,0 @@ -package defaultState - -import ( - "algobot/internal_old/helpers" - "algobot/internal_old/models" - "algobot/internal_old/serdes" - "algobot/internal_old/service" - "fmt" - "gopkg.in/telebot.v4" - "os" - "regexp" - "strconv" - "strings" -) - -var statuses = map[int]string{ - 0: "🟢 Учится", - 20: "🔴 Выбыл", - 10: "🟡 Переведен", -} - -type StartWithPayload struct { - svc service.Service -} - -func NewStartWithPayload(svc service.Service) *StartWithPayload { - return &StartWithPayload{svc: svc} -} - -func (s StartWithPayload) CanHandle(ctx telebot.Context) bool { - if strings.HasPrefix(ctx.Message().Text, "/start") { - return true - } - - return false -} - -func (s StartWithPayload) Process(ctx telebot.Context) error { - payload, err := serdes.Deserialize(ctx.Message().Payload) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка десериализации") - } - - switch payload.Action { - case models.GetGroupInfo: - return s.getGroupInfo(ctx, payload) - case models.GetKidInfo: - return s.getKidInfo(ctx, payload) - default: - return ctx.Send("Not supported") - } -} - -func (s StartWithPayload) getGroupInfo(ctx telebot.Context, payload models.StartPayload) error { - g, _ := strconv.Atoi(payload.Payload[0]) - full, err := s.svc.FullGroupInfo(ctx.Sender().ID, g) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при получении данной группы!") - } - - msg := GetGroupInfoMessage(full) - return ctx.Send(msg, telebot.ModeHTML, telebot.NoPreview) -} - -func GetGroupInfoMessage(full models.FullGroupInfo) string { - msg := strings.Builder{} - msg.WriteString(fmt.Sprintf("%s %s\n", full.GroupID, full.GroupTitle, full.GroupContent)) - msg.WriteString(fmt.Sprintf("\nСледующая лекция: %s\n", full.NextLessonTime)) - msg.WriteString(fmt.Sprintf("Всего пройдено %d лекций из %d\n", full.LessonsPassed, full.LessonsTotal)) - msg.WriteString(fmt.Sprintf("\nАктивные дети: %d | Выбыло: %d | Всего: %d\n", len(full.ActiveKids), len(full.NotActiveKids), len(full.ActiveKids)+len(full.NotActiveKids))) - msg.WriteString("Активные дети:\n") - for i, kid := range full.ActiveKids { - ser := serdes.Serialize(models.StartPayload{ - Action: models.GetKidInfo, - Payload: []string{strconv.Itoa(kid.ID), strconv.Itoa(full.GroupID)}, - }) - - msg.WriteString(fmt.Sprintf("%d. %s\n", i+1, os.Getenv("TELEGRAM_NAME"), ser, kid.FullName)) - } - msg.WriteString("Выбыли дети:\n") - for i, kid := range full.NotActiveKids { - ser := serdes.Serialize(models.StartPayload{ - Action: models.GetKidInfo, - Payload: []string{strconv.Itoa(kid.ID), strconv.Itoa(full.GroupID)}, - }) - - if kid.LastGroup.ID == full.GroupID { - msg.WriteString(fmt.Sprintf("%d. %s (🔴 Выбыл: %s)\n", i+1, os.Getenv("TELEGRAM_NAME"), ser, kid.FullName, kid.LastGroup.EndTime.Format("2006-01-02"))) - } else { - msg.WriteString(fmt.Sprintf("%d. %s (🟡 Переведен: %s)\n", i+1, os.Getenv("TELEGRAM_NAME"), ser, kid.FullName, kid.LastGroup.StartTime.Format("2006-01-02"))) - } - } - return msg.String() -} - -func (s StartWithPayload) getKidInfo(ctx telebot.Context, payload models.StartPayload) error { - - id, _ := strconv.Atoi(payload.Payload[0]) - groupId, _ := strconv.Atoi(payload.Payload[1]) - full, err := s.svc.FullKidInfo(ctx.Sender().ID, id, groupId) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при получении данного ученика!") - } - - m := GetKidInfoMessage(full) - return ctx.Send(m, telebot.ModeHTML, telebot.NoPreview) -} - -func GetKidInfoMessage(full models.FullKidInfo) string { - parentPhone := regexp.MustCompile(`[^0-9+]`).ReplaceAllString(full.Kid.Phone, "") - - msg := strings.Builder{} - if full.Extra == models.NotAccessible { - msg.WriteString(fmt.Sprintf("⚠️ У вас больше нету доступа к ребенку\n")) - } - msg.WriteString(fmt.Sprintf("%s\n", full.Kid.FullName)) - msg.WriteString(fmt.Sprintf("Возраст: %d\n", full.Kid.Age)) - msg.WriteString(fmt.Sprintf("День рождения: %s\n", full.Kid.BirthDate.Format("2006-01-02"))) - msg.WriteString("\nДанные от аккаунта:\n") - msg.WriteString(fmt.Sprintf("Логин: %s\n", full.Kid.Username)) - msg.WriteString(fmt.Sprintf("Пароль: %s\n", full.Kid.Password)) - msg.WriteString("\nРодитель:\n") - msg.WriteString(fmt.Sprintf("Имя: %s\n", full.Kid.ParentName)) - - msg.WriteString(fmt.Sprintf("Телефон: %s 🟩 Whatsapp\n", parentPhone, strings.TrimPrefix(parentPhone, "+"))) - msg.WriteString(fmt.Sprintf("Почта: %s\n", full.Kid.Email)) - msg.WriteString("\nГруппы\n") - - groups := full.Kid.Groups - for i := len(groups) - 1; i >= 0; i-- { - msg.WriteString(fmt.Sprintf("%d . %s %s\n", len(groups)-i, groups[i].ID, groups[i].Title, groups[i].Content)) - v, ok := statuses[groups[i].Status] - if !ok { - v = fmt.Sprintf("Статус [%d]", groups[i].Status) - } - msg.WriteString(fmt.Sprintf("%s (%s - %s)\n\n", v, groups[i].StartTime.Format("2006-01-02"), groups[i].EndTime.Format("2006-01-02"))) - } - m := msg.String() - return m -} diff --git a/internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go b/internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go deleted file mode 100644 index 4e7a385..0000000 --- a/internal_old/contextHandlers/textHandlers/sendingCookieState/rejectAction.go +++ /dev/null @@ -1,26 +0,0 @@ -package sendingCookieState - -import ( - "algobot/internal_old/config" - "algobot/internal_old/stateMachine" - "gopkg.in/telebot.v4" -) - -type RejectAction struct { - state stateMachine.StateMachine -} - -func NewRejectAction(state stateMachine.StateMachine) *RejectAction { - return &RejectAction{state: state} -} - -func (r RejectAction) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text == "Отменить действие" { - return true - } - return false -} -func (r RejectAction) Process(ctx telebot.Context) error { - r.state.SetStatement(ctx.Sender().ID, stateMachine.Default) - return ctx.Send(config.StartText, config.StartKeyboard) -} diff --git a/internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go b/internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go deleted file mode 100644 index d6c5f00..0000000 --- a/internal_old/contextHandlers/textHandlers/sendingCookieState/sendCookie.go +++ /dev/null @@ -1,38 +0,0 @@ -package sendingCookieState - -import ( - "algobot/internal_old/config" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "algobot/internal_old/stateMachine" - "gopkg.in/telebot.v4" -) - -type SendingCookieAction struct { - state stateMachine.StateMachine - service service.Service -} - -func NewSendingCookieAction(state stateMachine.StateMachine, service service.Service) *SendingCookieAction { - return &SendingCookieAction{state: state, service: service} -} - -func (s SendingCookieAction) CanHandle(ctx telebot.Context) bool { - if ctx.Message().Text != "Отменить действие" { - return true - } - return false -} - -func (s SendingCookieAction) Process(ctx telebot.Context) error { - uid := ctx.Sender().ID - cookie := ctx.Message().Text - - err := s.service.SetCookie(uid, cookie) - if err != nil { - return helpers.LogError(err, ctx, "Ошибка при установке Cookie!") - } - s.state.SetStatement(uid, stateMachine.Default) - - return ctx.Send(config.CookieSet, config.StartKeyboard) -} diff --git a/internal_old/domain/domain.go b/internal_old/domain/domain.go deleted file mode 100644 index 0951918..0000000 --- a/internal_old/domain/domain.go +++ /dev/null @@ -1,34 +0,0 @@ -package domain - -import ( - "time" -) - -type Group struct { - GroupID int - Title string - TimeLesson time.Time -} - -type User struct { - UID int64 - Cookie string - UserAgent string - Notifications bool - Groups []Group -} - -type Domain interface { - User(uid int64) (User, error) - Cookie(uid int64) (string, error) - SetCookie(uid int64, cookie string) error - SetUserAgent(uid int64, agent string) error - Groups(uid int64) ([]Group, error) - SetGroups(uid int64, groups []Group) error - Notification(uid int64) (bool, error) - SetNotification(uid int64, value bool) error - RegisterUser(uid int64) error - GetUsersByNotification(notifications int) ([]User, error) - LastNotificationDate(uid int64) (string, error) - SetLastNotificationDate(uid int64, data string) error -} diff --git a/internal_old/domain/sqlite3.go b/internal_old/domain/sqlite3.go deleted file mode 100644 index 49fa786..0000000 --- a/internal_old/domain/sqlite3.go +++ /dev/null @@ -1,349 +0,0 @@ -package domain - -import ( - appError "algobot/internal_old/error" - "database/sql" - "errors" - "fmt" - "io/fs" - "log" - "time" -) - -type Sqlite3 struct { - db *sql.DB -} - -func NewSqlite3(db *sql.DB) *Sqlite3 { - return &Sqlite3{db: db} -} - -func (s Sqlite3) GetUsersByNotification(notif int) ([]User, error) { - row, err := s.db.Query(`SELECT u.uid, u.cookie, u.user_agent, u.notification FROM users u WHERE notification = ?`, notif) - if err != nil { - return nil, fmt.Errorf("NewSqlite3.GetUsersByNotification(%d) : %w", notif, row.Err()) - } - - var users []User - for row.Next() { - var baseId sql.NullInt64 - var cookie sql.NullString - var userAgent sql.NullString - var notifications bool - - err := row.Scan(&baseId, &cookie, &userAgent, ¬ifications) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, fmt.Errorf("NewSqlite3.GetUsersByNotification(%d) : %w", notif, appError.ErrHasNone) - } - return nil, fmt.Errorf("NewSqlite3.GetUsersByNotification(%d) : %w", notif, row.Err()) - } - - u := User{ - UID: baseId.Int64, - Cookie: cookie.String, - UserAgent: userAgent.String, - Notifications: notifications, - Groups: nil, - } - users = append(users, u) - s.appendGroups(&u, baseId.Int64) - } - - return users, nil -} - -func (s Sqlite3) LastNotificationDate(uid int64) (string, error) { - row := s.db.QueryRow(`SELECT u.last_notification_msg FROM users u WHERE u.uid = ?`, uid) - - if row.Err() != nil { - return "", fmt.Errorf("NewSqlite3.LastNotificationDate(%d) : %w", uid, row.Err()) - } - - var notifString sql.NullString - - err := row.Scan(¬ifString) - if err != nil { - return "", fmt.Errorf("NewSqlite3.LastNotificationDate(%d) : %w", uid, err) - } - - if !notifString.Valid || notifString.String == "" { - return "", fmt.Errorf("NewSqlite3.LastNotificationDate(%d) : %w", uid, appError.ErrNotValid) - } - return notifString.String, nil -} - -func (s Sqlite3) SetLastNotificationDate(uid int64, data string) error { - _, err := s.db.Exec("UPDATE users SET last_notification_msg=? WHERE uid=?", data, uid) - if err != nil { - return fmt.Errorf("NewSqlite3.SetLastNotificationDate(%d, %s) : %w", uid, data, err) - } - return nil -} - -func (s Sqlite3) User(uid int64) (User, error) { - row := s.db.QueryRow(`SELECT u.uid, u.cookie, u.user_agent, u.notification FROM users u WHERE u.uid = ?`, uid) - if row.Err() != nil { - return User{}, fmt.Errorf("NewSqlite3.User(%d) : %w", uid, row.Err()) - } - - var baseId sql.NullInt64 - var cookie sql.NullString - var userAgent sql.NullString - var notifications bool - - err := row.Scan(&baseId, &cookie, &userAgent, ¬ifications) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return User{}, fmt.Errorf("NewSqlite3.User(%d) : %w", uid, appError.ErrNotFound) - } - return User{}, fmt.Errorf("NewSqlite3.User(%d) : %w", uid, err) - } - - if !baseId.Valid { - return User{}, fmt.Errorf("NewSqlite3.User(%d) : %w", uid, appError.ErrNotValid) - } - - u := User{ - Cookie: cookie.String, - UserAgent: userAgent.String, - Notifications: notifications, - } - - s.appendGroups(&u, baseId.Int64) - - return u, nil -} - -func (s Sqlite3) Cookie(uid int64) (string, error) { - row := s.db.QueryRow(`SELECT u.cookie FROM users u WHERE u.uid = ?`, uid) - - if row.Err() != nil { - return "", fmt.Errorf("NewSqlite3.Cookie(%d) : %w", uid, row.Err()) - } - - var cookie sql.NullString - - err := row.Scan(&cookie) - if err != nil { - return "", fmt.Errorf("NewSqlite3.Cookie(%d) : %w", uid, err) - } - - if !cookie.Valid { - return "", fmt.Errorf("NewSqlite3.Cookie(%d) : %w", uid, appError.ErrNotValid) - } - return cookie.String, nil -} - -func (s Sqlite3) SetCookie(uid int64, cookie string) error { - _, err := s.db.Exec("UPDATE users SET cookie=? WHERE uid=?", cookie, uid) - if err != nil { - return fmt.Errorf("NewSqlite3.SetCookie(%d, %s) : %w", uid, cookie, err) - } - return nil -} - -func (s Sqlite3) SetUserAgent(uid int64, agent string) error { - _, err := s.db.Exec("UPDATE users SET user_agent=? WHERE uid= ?;", agent, uid) - if err != nil { - return fmt.Errorf("NewSqlite3.SetUserAgent(%d, %s) : %w", uid, agent, err) - } - return nil -} - -func (s Sqlite3) Groups(uid int64) ([]Group, error) { - - rows, err := s.db.Query(`SELECT g.group_id, g.title, g.time_lesson - FROM groups g - WHERE g.owner_id = ?;`, uid) - if err != nil { - return nil, fmt.Errorf("NewSqlite3.Groups(%d) : %w", uid, err) - } - defer rows.Close() - - groups := make([]Group, 0) - for rows.Next() { - var groupId sql.NullInt64 - var title sql.NullString - var timeGroup sql.NullString - - if err := rows.Scan(&groupId, &title, &timeGroup); err != nil { - return nil, fmt.Errorf("NewSqlite3.Groups(%d) : %w", uid, err) - } - - parsedTime, err := time.Parse("2006-01-02 15:04:05", timeGroup.String) - if err != nil { - return nil, fmt.Errorf("NewSqlite3.Groups(%d) : %w", uid, err) - } - - groups = append(groups, Group{ - GroupID: int(groupId.Int64), - Title: title.String, - TimeLesson: parsedTime, - }) - } - - if len(groups) == 0 { - return nil, fmt.Errorf("NewSqlite3.Groups(%d) : %w", uid, appError.ErrHasNone) - } - return groups, nil -} - -func (s Sqlite3) SetGroups(uid int64, groups []Group) error { - tx, err := s.db.Begin() - if err != nil { - return fmt.Errorf("NewSqlite3.SetGroups(%d, %#v) : %w", uid, groups, err) - } - if _, err := tx.Exec("DELETE FROM groups WHERE owner_id=?", uid); err != nil { - tx.Rollback() - return fmt.Errorf("NewSqlite3.SetGroups(%d, %#v) : %w", uid, groups, err) - } - - stmt, err := tx.Prepare("INSERT INTO groups (group_id, owner_id, title, time_lesson) VALUES (?, ?, ?, ?)") - if err != nil { - tx.Rollback() - return fmt.Errorf("NewSqlite3.SetGroups(%d, %#v) : %w", uid, groups, err) - } - defer stmt.Close() - - for _, g := range groups { - if _, err := stmt.Exec(g.GroupID, uid, g.Title, g.TimeLesson.Format("2006-01-02 15:04:05")); err != nil { - tx.Rollback() - return fmt.Errorf("NewSqlite3.SetGroups(%d, %#v) : %w", uid, groups, err) - } - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("NewSqlite3.SetGroups(%d, %#v) : %w", uid, groups, err) - } - - return nil -} - -func (s Sqlite3) Notification(uid int64) (bool, error) { - row := s.db.QueryRow("SELECT notification FROM users WHERE uid=?", uid) - - var notif sql.NullBool - if err := row.Scan(¬if); err != nil { - return false, fmt.Errorf("NewSqlite3.Notification(%d) : %w", uid, err) - } - - if !notif.Valid { - return false, fmt.Errorf("NewSqlite3.Notification(%d) : %w", uid, appError.ErrNotValid) - } - - return notif.Bool, nil -} -func (s Sqlite3) SetNotification(uid int64, notification bool) error { - digit := 0 - if notification { - digit = 1 - } - - _, err := s.db.Exec("UPDATE users SET notification=? WHERE uid=?", digit, uid) - if err != nil { - return fmt.Errorf("NewSqlite3.SetNotification(%d, %v) : %w", uid, notification, err) - } - return nil -} -func (s Sqlite3) RegisterUser(uid int64) error { - _, err := s.db.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES (?, NULL, NULL, 0)", uid) - if err != nil { - return fmt.Errorf("NewSqlite3.RegisterUser(%d) : %w", uid, err) - } - return nil -} - -func (s Sqlite3) appendGroups(u *User, id int64) { - sqlQuery := "SELECT g.group_id, g.title, g.time_lesson FROM groups g WHERE g.owner_id = ?" - query, err := s.db.Query(sqlQuery, id) - if err != nil { - log.Printf("Ошибка при выполнении запроса, %s, со значением %v", sqlQuery, id) - return - } - defer query.Close() - for query.Next() { - var groupId sql.NullInt64 - var title sql.NullString - var timeGroup sql.NullString - - query.Scan(&groupId, &title, &timeGroup) - - parsedTime, err := time.Parse("2006-01-02 15:04:05", timeGroup.String) - if err != nil { - log.Printf("2 Ошибка при парсинге даты %v - %v", timeGroup.String, err) - return - } - - u.Groups = append(u.Groups, Group{ - GroupID: int(groupId.Int64), - Title: title.String, - TimeLesson: parsedTime, - }) - } -} - -func (s Sqlite3) Migrate(eFs fs.FS, dir string) { - log.Println("Начинаю процесс миграции базы") - files, err := fs.ReadDir(eFs, dir) - if err != nil { - log.Fatal(err) - } - - if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS migrations ( - name TEXT PRIMARY KEY, - executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`); err != nil { - log.Fatalf("ошибка создания таблицы миграций: %v\n", err) - } - - for _, file := range files { - if file.IsDir() { - continue - } - log.Printf("Миграция файла - %s\n", file.Name()) - var exists bool - err := s.db.QueryRow( - `SELECT EXISTS (SELECT 1 FROM migrations WHERE name = ?)`, - file.Name(), - ).Scan(&exists) - if err != nil { - log.Fatalf("Ошибка проверки таблицы: %v\n", err) - } - if exists { - log.Printf("Миграция %s, уже существует\n", file.Name()) - continue - } - - content, err := fs.ReadFile(eFs, "migrations/"+file.Name()) - if err != nil { - log.Fatalf("ошибка чтения файла %s: %v", file.Name(), err) - } - _ = content - - tx, err := s.db.Begin() - if err != nil { - log.Fatal(err) - } - - if _, err := tx.Exec(string(content)); err != nil { - tx.Rollback() - log.Fatalf("ошибка выполнения миграции %s: %w", file.Name(), err) - } - - if _, err := tx.Exec( - "INSERT INTO migrations (name) VALUES (?)", - file.Name(), - ); err != nil { - tx.Rollback() - log.Fatalf("ошибка записи миграции %s: %w", file.Name(), err) - } - - if err := tx.Commit(); err != nil { - log.Fatalf("ошибка коммита транзакции: %w", err) - } - - log.Printf("Применена миграция: %s\n", file.Name()) - } - - log.Print("База данных готова к использованию!\n") -} diff --git a/internal_old/error/error.go b/internal_old/error/error.go deleted file mode 100644 index 0189de7..0000000 --- a/internal_old/error/error.go +++ /dev/null @@ -1,10 +0,0 @@ -package appError - -import ( - "errors" -) - -var ErrNotValid = errors.New("not valid") -var ErrNotFound = errors.New("not found") -var ErrHasNone = errors.New("has no one") -var NotEnoughArgs = errors.New("not enough arguments") diff --git a/internal_old/helpers/group.go b/internal_old/helpers/group.go deleted file mode 100644 index fe0c9eb..0000000 --- a/internal_old/helpers/group.go +++ /dev/null @@ -1,68 +0,0 @@ -package helpers - -import ( - appError "algobot/internal_old/error" - "algobot/internal_old/models" - "sort" - "time" -) - -// GetSortedGroups получение списка групп отсортированных по дням -// И внутри дней по часам -func GetSortedGroups(groups []models.Group) []models.Group { - sort.Slice(groups, func(i, j int) bool { - dayI, dayJ := groups[i].TimeLesson.Weekday(), groups[j].TimeLesson.Weekday() - if dayI != dayJ { - return dayI > dayJ - } - return groups[i].TimeLesson.Hour() < groups[j].TimeLesson.Hour() || - (groups[i].TimeLesson.Hour() == groups[j].TimeLesson.Hour() && groups[i].TimeLesson.Minute() < groups[j].TimeLesson.Minute()) - }) - return groups -} - -// GetCurrentGroup , логика выдачи групп: -// За 30 минут до начала и во время группы выдывать конкретную группу -func GetCurrentGroup(t time.Time, g []models.Group) (models.Group, error) { - current, err := GetGroupsByDay(t, g) - if err != nil { - return models.Group{}, err - } - for _, group := range current { - if inDiapazon(-30, 90, t, group.TimeLesson) { - return group, nil - } - } - - return models.Group{}, appError.ErrHasNone -} - -// GetGroupsByDay получение групп по текущему дню -func GetGroupsByDay(t time.Time, g []models.Group) ([]models.Group, error) { - var filtered []models.Group - - for _, group := range g { - if t.Weekday() == group.TimeLesson.Weekday() { - filtered = append(filtered, group) - } - } - if len(filtered) == 0 { - return nil, appError.ErrHasNone - } - - return filtered, nil -} - -func inDiapazon(start, end int, now, group time.Time) bool { - s := group.Add(time.Duration(start) * time.Minute) - e := group.Add(time.Duration(end) * time.Minute) - - startTime := time.Date(1970, 1, 1, s.Hour(), s.Minute(), 0, 0, time.UTC) - endTime := time.Date(1970, 1, 1, e.Hour(), e.Minute(), 0, 0, time.UTC) - currentTime := time.Date(1970, 1, 1, now.Hour(), now.Minute(), 0, 0, time.UTC) - - if currentTime.After(startTime) && currentTime.Before(endTime) { - return true - } - return false -} diff --git a/internal_old/helpers/group_test.go b/internal_old/helpers/group_test.go deleted file mode 100644 index bc7d37c..0000000 --- a/internal_old/helpers/group_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package helpers - -import ( - "algobot/internal_old/models" - "reflect" - "testing" - "time" -) - -func Test_GetGroupsByDay(t *testing.T) { - all := []models.Group{ - { - GroupID: 1, - Title: "Lesson 1", - TimeLesson: getDayByTime(21, 12, 30), - }, - { - GroupID: 2, - Title: "Lesson 2", - TimeLesson: getDayByTime(27, 14, 00), - }, - { - GroupID: 3, - Title: "Lesson 3", - TimeLesson: getDayByTime(26, 14, 00), - }, - { - GroupID: 4, - Title: "Lesson 4", - TimeLesson: getDayByTime(28, 15, 30), - }, - } - t.Run("If groups exists", func(t *testing.T) { - g, e := GetGroupsByDay(getDayByTime(14, 9, 9), all) - if e != nil { - t.Fatalf("Unexpected error %v", e) - } - want := []models.Group{ - all[0], - all[3], - } - if reflect.DeepEqual(want, g) == false { - t.Fatalf("Expected: %v, Got: %v", want, g) - } - }) - t.Run("If groups not exists", func(t *testing.T) { - _, e := GetGroupsByDay(getDayByTime(18, 9, 9), all) - if e == nil { - t.Fatalf("Wanted error, got nil!") - } - }) -} - -func Test_GetCurrentGroup(t *testing.T) { - all := []models.Group{ - { - GroupID: 1, - Title: "Lesson 1", // вск - TimeLesson: getDayByTime(21, 12, 30), - }, - { - GroupID: 2, - Title: "Lesson 2", // суб - TimeLesson: getDayByTime(27, 14, 00), - }, - { - GroupID: 3, - Title: "Lesson 3", // птн - TimeLesson: getDayByTime(26, 14, 00), - }, - { - GroupID: 4, - Title: "Lesson 4", // вск - TimeLesson: getDayByTime(28, 15, 30), - }, - } - t.Run("If group exists", func(t *testing.T) { - date := getDayByTime(14, 12, 10) - g, e := GetCurrentGroup(date, all) - if e != nil { - t.Fatalf("Unexpected error %v", e) - } - want := all[0] - if reflect.DeepEqual(want, g) == false { - t.Fatalf("Expected: %v, Got: %v", want, g) - } - }) - t.Run("If group end", func(t *testing.T) { - date := getDayByTime(14, 21, 10) - _, e := GetCurrentGroup(date, all) - if e == nil { - t.Fatalf("Expected error, got nil") - } - }) -} - -// getDayByTime 28, 21 вск || 27, 20 сб -func getDayByTime(day, hour, min int) time.Time { - return time.Date(2025, 9, day, hour, min, 0, 0, time.UTC) -} diff --git a/internal_old/helpers/logError.go b/internal_old/helpers/logError.go deleted file mode 100644 index 1bd32de..0000000 --- a/internal_old/helpers/logError.go +++ /dev/null @@ -1,32 +0,0 @@ -package helpers - -import ( - "fmt" - "gopkg.in/telebot.v4" - "log" - "math/rand" - "time" -) - -func token() string { - charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - token := make([]byte, 7) - for i := range token { - token[i] = charset[rng.Intn(len(charset))] - } - - return string(token) -} - -func LogError(err error, ctx telebot.Context, reason string) error { - token := token() - - log.Printf("ERR: %s | %s\n", token, err.Error()) - if reason == "" { - reason = "Произошла ошибка, классифицировать не удалось :(" - } - - return ctx.Send(fmt.Sprintf("[%s] %s", token, reason), telebot.ModeHTML) -} diff --git a/internal_old/middleware/logger.go b/internal_old/middleware/logger.go deleted file mode 100644 index 4ab85c2..0000000 --- a/internal_old/middleware/logger.go +++ /dev/null @@ -1,18 +0,0 @@ -package middleware - -import ( - tele "gopkg.in/telebot.v4" - "log" -) - -func MessageLogger(next tele.HandlerFunc) tele.HandlerFunc { - return func(c tele.Context) error { - if c.Callback() != nil { - log.Printf("[CLB] %s, запрос \"%s\"\n", c.Callback().Sender.FirstName+c.Callback().Sender.LastName, c.Callback().Data) - } else { - log.Printf("[MSG] %s, запрос \"%s\"\n", c.Message().Sender.FirstName+c.Message().Sender.LastName, c.Message().Text) - } - - return next(c) - } -} diff --git a/internal_old/middleware/register.go b/internal_old/middleware/register.go deleted file mode 100644 index bbc8509..0000000 --- a/internal_old/middleware/register.go +++ /dev/null @@ -1,38 +0,0 @@ -package middleware - -import ( - "algobot/internal_old/config" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/service" - "errors" - "gopkg.in/telebot.v4" -) - -type Register struct { - svc service.Service -} - -func NewRegister(svc service.Service) *Register { - return &Register{svc: svc} -} - -func (r *Register) Middleware(next telebot.HandlerFunc) telebot.HandlerFunc { - return func(context telebot.Context) error { - uid := context.Sender().ID - - reg, err := r.svc.IsUserRegistered(uid) - if err != nil && !errors.Is(err, appError.ErrNotFound) { - return helpers.LogError(err, context, "Произошла ошибка при проверки регистрации!") - } - if reg == false { - err := r.svc.RegisterUser(uid) - if err != nil { - return helpers.LogError(err, context, "Произошла ошибка при регистрации!") - } - context.Send(config.HelloWorld, config.StartKeyboard) - } - - return next(context) - } -} diff --git a/internal_old/models/models.go b/internal_old/models/models.go deleted file mode 100644 index 1b7418a..0000000 --- a/internal_old/models/models.go +++ /dev/null @@ -1,81 +0,0 @@ -package models - -import ( - "algobot/internal_old/clients" - "algobot/internal_old/domain" - "time" -) - -type MissingKid struct { - Id int - Count int -} - -type ActualInformation struct { - LessonTitle string - LessonId int - MissingKids []MissingKid -} - -type KidData struct { - FullName string - Login string - Password string -} -type ScheduleData struct { - UID int64 - Cookie string -} - -type Message struct { - Id string - From string - Theme string - Type string - Link string - Content string -} - -type AllKids map[int]KidData - -type Group struct { - GroupID int - Title string - TimeLesson time.Time -} - -func GroupMap(domains []domain.Group) []Group { - gr := make([]Group, len(domains)) - for i, group := range domains { - gr[i] = mapGroup(group) - } - return gr -} - -func mapGroup(group domain.Group) Group { - return Group{ - GroupID: group.GroupID, - Title: group.Title, - TimeLesson: group.TimeLesson, - } -} - -type FullGroupInfo struct { - GroupID int - GroupTitle string - GroupContent string - NextLessonTime string - LessonsTotal int - LessonsPassed int - ActiveKids []clients.Student - NotActiveKids []clients.Student -} - -type ExtraInfo string - -var NotAccessible ExtraInfo = "not_accessible" - -type FullKidInfo struct { - Extra ExtraInfo - Kid clients.Student -} diff --git a/internal_old/models/startPayload.go b/internal_old/models/startPayload.go deleted file mode 100644 index 75739b8..0000000 --- a/internal_old/models/startPayload.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -type ActionType string - -var GetGroupInfo ActionType = "getGroupInfo" -var GetKidInfo ActionType = "getKidInfo" - -type StartPayload struct { - Action ActionType - Payload []string -} diff --git a/internal_old/schedulers/message.go b/internal_old/schedulers/message.go deleted file mode 100644 index ec663e4..0000000 --- a/internal_old/schedulers/message.go +++ /dev/null @@ -1,63 +0,0 @@ -package schedulers - -import ( - "algobot/internal_old/models" - "algobot/internal_old/service" - "fmt" - "gopkg.in/telebot.v4" - "log" - "strconv" - "strings" -) - -type Message struct { - b telebot.API - svc service.Service -} - -func NewMessage(b telebot.API, svc service.Service) *Message { - return &Message{b: b, svc: svc} -} - -func (m Message) Schedule() { - users, err := m.svc.UsersByNotif(true) - if err != nil { - log.Println(err) - } - for _, user := range users { - allMessages, err := m.svc.NewMessageByUID(user.UID) - if err != nil { - log.Println(err) - } - for _, msg := range allMessages { - if msg.Type == "img" { - p := &telebot.Photo{File: telebot.FromURL(msg.Content), Caption: getMsg(msg)} - m.b.Send(RecipientUser{strconv.FormatInt(user.UID, 10)}, p, telebot.ModeMarkdown, telebot.NoPreview) - } else { - m.b.Send(RecipientUser{strconv.FormatInt(user.UID, 10)}, getMsg(msg), telebot.ModeMarkdown, telebot.NoPreview) - } - } - } -} - -func getMsg(msg models.Message) string { - sb := strings.Builder{} - sb.WriteString("🔔 Новое сообщение\n\n") - sb.WriteString(fmt.Sprintf("От: %s\n", msg.From)) - sb.WriteString(fmt.Sprintf("Тема: %s\n", msg.Theme)) - sb.WriteString(fmt.Sprintf("Ссылка: %s\n\n", msg.Link)) - if msg.Type != "img" { - sb.WriteString("```Сообщение:\n") - sb.WriteString(msg.Content) - sb.WriteString("\n```") - } - return sb.String() -} - -type RecipientUser struct { - ID string -} - -func (r RecipientUser) Recipient() string { - return r.ID -} diff --git a/internal_old/serdes/simple.go b/internal_old/serdes/simple.go deleted file mode 100644 index f430e6d..0000000 --- a/internal_old/serdes/simple.go +++ /dev/null @@ -1,25 +0,0 @@ -package serdes - -import ( - "algobot/internal_old/models" - "fmt" - "github.com/jxskiss/base62" - "strings" -) - -func Serialize(m models.StartPayload) string { - return base62.EncodeToString([]byte(fmt.Sprintf("%s-%s", m.Action, strings.Join(m.Payload, "-")))) -} -func Deserialize(s string) (models.StartPayload, error) { - decodeString, err := base62.DecodeString(s) - if err != nil { - return models.StartPayload{}, err - } - arr := strings.Split(string(decodeString), "-") - - m := models.StartPayload{ - Action: models.ActionType(arr[0]), - Payload: arr[1:], - } - return m, nil -} diff --git a/internal_old/service/AIService.go b/internal_old/service/AIService.go deleted file mode 100644 index 4d3bdf7..0000000 --- a/internal_old/service/AIService.go +++ /dev/null @@ -1,55 +0,0 @@ -package service - -import ( - pkg "algobot/protos" - "context" - "fmt" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "log" -) - -type AIService interface { - GetSuggestion(uid int64, text string) (string, error) - ClearAllHistory(uid int64) error -} - -type aiService struct { - grpc pkg.AiClient -} - -func NewAiService(port string) AIService { - conn, err := grpc.NewClient("localhost:"+port, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Fatalf("did not connect: %v", err) - } - - return &aiService{ - grpc: pkg.NewAiClient(conn), - } -} - -func (a aiService) GetSuggestion(uid int64, text string) (string, error) { - ctx := context.Background() - suggest, err := a.grpc.GetSuggest(ctx, &pkg.SuggestRequest{ - Uid: uid, - Suggest: text, - }) - if err != nil { - return "", fmt.Errorf("aiService.GetSuggestion(%v, %v) : %w", uid, text, err) - } - - return suggest.GetRequest(), nil -} - -func (a aiService) ClearAllHistory(uid int64) error { - ctx := context.Background() - _, err := a.grpc.ClearHistory(ctx, &pkg.ClearHistoryRequest{ - Uid: uid, - }) - if err != nil { - return fmt.Errorf("aiService.ClearAllHistory(%v) : %w", uid, err) - } - - return nil -} diff --git a/internal_old/service/DefaultService.go b/internal_old/service/DefaultService.go deleted file mode 100644 index fc6a66c..0000000 --- a/internal_old/service/DefaultService.go +++ /dev/null @@ -1,428 +0,0 @@ -package service - -import ( - "algobot/internal_old/clients" - "algobot/internal_old/domain" - appError "algobot/internal_old/error" - "algobot/internal_old/helpers" - "algobot/internal_old/models" - "errors" - "fmt" - "log" - "regexp" - "strconv" - "strings" - "time" -) - -var dateMap map[string]string = map[string]string{ - "янв": "01", - "февр": "02", - "мар": "03", - "апр": "04", - "мая": "05", - "июн": "06", - "июл": "07", - "авг": "08", - "сент": "09", - "окт": "10", - "нояб": "11", - "дек": "12", -} - -type DefaultService struct { - domain domain.Domain - webClient clients.WebClient -} - -func NewDefaultService(domain domain.Domain, webClient clients.WebClient) *DefaultService { - return &DefaultService{domain: domain, webClient: webClient} -} - -func (d DefaultService) FullKidInfo(uid int64, kidID int, groupId int) (models.FullKidInfo, error) { - cookie, err := d.Cookie(uid) - if err != nil { - return models.FullKidInfo{}, fmt.Errorf("DefaultService.FullKidInfo(%d, %d) : %w", uid, kidID, err) - } - kid, err := d.webClient.GetKidInfo(cookie, strconv.Itoa(kidID)) - if err != nil { - if errors.Is(err, appError.ErrNotFound) { - info, err := d.webClient.GetKidsNamesByGroup(cookie, groupId) - if err != nil { - return models.FullKidInfo{}, fmt.Errorf("DefaultService.FullKidInfo(%d, %d) : %w", uid, kidID, err) - } - for _, item := range info.Data.Items { - if item.ID == kidID { - return models.FullKidInfo{ - Extra: models.NotAccessible, - Kid: item, - }, nil - } - } - } - return models.FullKidInfo{}, fmt.Errorf("DefaultService.FullKidInfo(%d, %d) : %w", uid, kidID, err) - } - - return models.FullKidInfo{ - Kid: kid.Data, - }, nil -} - -func (d DefaultService) UsersByNotif(status bool) ([]models.ScheduleData, error) { - notif := 0 - if status { - notif = 1 - } - - notification, err := d.domain.GetUsersByNotification(notif) - if err != nil { - return nil, fmt.Errorf("DefaultService.UserUidsByNotif(%v) : %w", status, err) - } - - data := make([]models.ScheduleData, len(notification)) - for i, user := range notification { - data[i] = models.ScheduleData{ - UID: user.UID, - Cookie: user.Cookie, - } - } - return data, nil -} - -func (d DefaultService) NewMessageByUID(uid int64) ([]models.Message, error) { - cookie, err := d.Cookie(uid) - if err != nil { - return nil, fmt.Errorf("DefaultService.NewMessageByUID(%d) : %w", uid, err) - } - messages, err := d.webClient.GetKidsMessages(cookie) - if err != nil { - return nil, fmt.Errorf("DefaultService.NewMessageByUID(%d) : %w", uid, err) - } - - var msgs []models.Message - lastNotif, err := d.domain.LastNotificationDate(uid) - var lastNotifData time.Time - if err != nil { - if !errors.Is(err, appError.ErrNotValid) { - return nil, fmt.Errorf("DefaultService.NewMessageByUID(%d) : %w", uid, err) - } - lastNotifData = time.Time{} - } else { - lastNotifData = parseDate(lastNotif) - } - var lastNotifString string - for i := len(messages.Data.Projects) - 1; i >= 0; i-- { - if messages.Data.Projects[i].SenderScope == "student" { - if lastNotifData.Before(parseDate(messages.Data.Projects[i].LastTime)) { - m := models.Message{ - Id: messages.Data.Projects[i].UID, - Type: messages.Data.Projects[i].Type, - From: messages.Data.Projects[i].Name, - Theme: messages.Data.Projects[i].Title, - Link: fmt.Sprintf("https://backoffice.algoritmika.org%s", messages.Data.Projects[i].Link), - Content: messages.Data.Projects[i].Content, - } - if m.Type == "img" { - m.Content = fmt.Sprintf("https://backoffice.algoritmika.org%s", m.Content) - } - msgs = append(msgs, m) - lastNotifString = messages.Data.Projects[i].LastTime - } - } - } - if lastNotifString != "" { - err := d.domain.SetLastNotificationDate(uid, lastNotifString) - if err != nil { - return nil, fmt.Errorf("DefaultService.NewMessageByUID(%d) : %w", uid, err) - } - } - - return msgs, nil -} - -func parseDate(lastTime string) time.Time { - parts := strings.Split(lastTime, " ") - - day := parts[0] - month := dateMap[strings.Replace(parts[1], ".", "", -1)] - - if len(parts) == 3 { - timeHour := parts[2] - - retTime, _ := time.Parse("2 01 2006 15:04", fmt.Sprintf("%s %s %d %s", day, month, time.Now().Year(), timeHour)) - return retTime - } - if len(parts) == 4 { - year := strings.Replace(parts[2], "`", "", -1) - year = strings.Replace(year, ",", "", -1) - timeHour := parts[3] - - retTime, _ := time.Parse("2 01 06 15:04", fmt.Sprintf("%s %s %s %s", day, month, year, timeHour)) - return retTime - } - return time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC) -} - -func (d DefaultService) FullGroupInfo(uid int64, groupId int) (models.FullGroupInfo, error) { - cookie, err := d.Cookie(uid) - if err != nil { - return models.FullGroupInfo{}, fmt.Errorf("DefaultService.FullGroupInfo(%d, %d) : %w", uid, groupId, err) - } - retObject := models.FullGroupInfo{} - info, err := d.webClient.GetGroupInfo(cookie, strconv.Itoa(groupId)) - if err != nil { - return models.FullGroupInfo{}, fmt.Errorf("DefaultService.FullGroupInfo(%d, %d) : %w", uid, groupId, err) - } - retObject.GroupID = info.Data.ID - retObject.GroupTitle = info.Data.Title - retObject.GroupContent = info.Data.Content - retObject.NextLessonTime = info.Data.NextLessonTime - retObject.LessonsPassed = info.Data.LessonsPassed - retObject.LessonsTotal = info.Data.LessonsTotal - - names, err := d.webClient.GetKidsNamesByGroup(cookie, groupId) - if err != nil { - return models.FullGroupInfo{}, fmt.Errorf("DefaultService.FullGroupInfo(%d, %d) : %w", uid, groupId, err) - } - for _, item := range names.Data.Items { - if item.LastGroup.ID == groupId && item.LastGroup.Status == 0 { - retObject.ActiveKids = append(retObject.ActiveKids, item) - } else { - retObject.NotActiveKids = append(retObject.NotActiveKids, item) - } - } - - return retObject, nil -} - -func (d DefaultService) OpenLesson(uid int64, groupId int, lessonId int) error { - cookie, err := d.Cookie(uid) - if err != nil { - return fmt.Errorf("DefaultService.OpenLesson(%d, %d, %d) : %w", uid, lessonId, groupId, err) - } - err = d.webClient.OpenLession(cookie, strconv.Itoa(groupId), strconv.Itoa(lessonId)) - if err != nil { - return fmt.Errorf("DefaultService.OpenLesson(%d, %d, %d) : %w", uid, lessonId, groupId, err) - } - - return nil -} -func (d DefaultService) CloseLesson(uid int64, groupId int, lessonId int) error { - cookie, err := d.Cookie(uid) - if err != nil { - return fmt.Errorf("DefaultService.CloseLesson(%d, %d, %d) : %w", uid, lessonId, groupId, err) - } - err = d.webClient.CloseLession(cookie, strconv.Itoa(groupId), strconv.Itoa(lessonId)) - if err != nil { - return fmt.Errorf("DefaultService.CloseLesson(%d, %d, %d) : %w", uid, lessonId, groupId, err) - } - - return nil -} - -func (d DefaultService) AllCredentials(uid int64, groupId int) (map[string]string, error) { - names, err := d.AllKidsNames(uid, groupId) - if err != nil { - return nil, fmt.Errorf("DefaultService.AllCredentials(%d, %d) : %w", uid, groupId, err) - } - - creds := make(map[string]string, len(names)) - for _, kid := range names { - creds[kid.FullName] = fmt.Sprintf("%s:%s", kid.Login, kid.Password) - } - - return creds, nil -} - -func (d DefaultService) Groups(uid int64) ([]models.Group, error) { - data, err := d.domain.Groups(uid) - if err != nil { - return nil, fmt.Errorf("DefaultService.Groups(%d) : %w", uid, err) - } - return models.GroupMap(data), nil -} - -func (d DefaultService) Cookie(uid int64) (string, error) { - c, err := d.domain.Cookie(uid) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return "", nil - } - return "", fmt.Errorf("DefaultService.Cookie(%d) : %w", uid, err) - } - return c, nil -} -func (d DefaultService) SetCookie(uid int64, cookie string) error { - err := d.domain.SetCookie(uid, cookie) - if err != nil { - return fmt.Errorf("DefaultService.SetCookie(%d, %s) : %w", uid, cookie, err) - } - return nil -} - -func (d DefaultService) Notification(uid int64) (bool, error) { - n, err := d.domain.Notification(uid) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return false, nil - } - return false, fmt.Errorf("DefaultService.Notification(%d) : %w", uid, err) - } - return n, nil -} -func (d DefaultService) SetNotification(uid int64, notification bool) error { - err := d.domain.SetNotification(uid, notification) - if err != nil { - return fmt.Errorf("DefaultService.SetNotification(%d, %v) : %w", uid, notification, err) - } - return nil -} - -func (d DefaultService) CurrentGroup(uid int64, t time.Time) (models.Group, error) { - allGroups, err := d.Groups(uid) - if err != nil { - return models.Group{}, fmt.Errorf("DefaultService.CurrentGroup(%d, %v) : %w", uid, t, err) - } - - group, err := helpers.GetCurrentGroup(t, allGroups) - if err != nil { - return models.Group{}, fmt.Errorf("DefaultService.CurrentGroup(%d, %v) : %w", uid, t, err) - } - - return group, nil -} -func (d DefaultService) ActualInformation(uid int64, t time.Time, groupId int) (models.ActualInformation, error) { - cookie, err := d.Cookie(uid) - if err != nil { - return models.ActualInformation{}, fmt.Errorf("DefaultService.ActualInformation(%d, %v, %d) : %w", uid, t, groupId, err) - } - stats, err := d.webClient.GetKidsStatsByGroup(cookie, strconv.Itoa(groupId)) - if err != nil { - return models.ActualInformation{}, fmt.Errorf("DefaultService.ActualInformation(%d, %v, %d) : %w", uid, t, groupId, err) - } - - actual := models.ActualInformation{} - for _, datum := range stats.Data { - studentID := datum.StudentID - count := 0 - for _, attendance := range datum.Attendance { - count++ - if attendance.Status != "absent" { - count = 0 - } - if matchDates(attendance.StartTimeFormatted, t) { - actual.LessonTitle = attendance.LessonTitle - actual.LessonId = attendance.LessonID - - if attendance.Status == "absent" { - actual.MissingKids = append(actual.MissingKids, models.MissingKid{ - Id: studentID, - Count: count, - }) - break - } - } - } - } - - return actual, nil -} -func (d DefaultService) AllKidsNames(uid int64, groupId int) (models.AllKids, error) { - cookie, err := d.Cookie(uid) - if err != nil { - return nil, fmt.Errorf("DefaultService.AllKidsNames(%d, %d) : %w", uid, groupId, err) - } - group, err := d.webClient.GetKidsNamesByGroup(cookie, groupId) - if err != nil { - return nil, fmt.Errorf("DefaultService.AllKidsNames(%d, %d) : %w", uid, groupId, err) - } - - names := make(map[int]models.KidData, len(group.Data.Items)) - for _, datum := range group.Data.Items { - if datum.LastGroup.ID == groupId && datum.LastGroup.Status == 0 { - names[datum.ID] = models.KidData{ - FullName: datum.FullName, - Login: datum.Username, - Password: datum.Password, - } - } - } - - return names, nil -} - -func (d DefaultService) RefreshGroups(uid int64) error { - cookie, err := d.Cookie(uid) - if err != nil { - return fmt.Errorf("DefaultService.RefreshGroups(%d) : %w", uid, err) - } - groups, err := d.webClient.GetAllGroupsByUser(cookie) - if err != nil { - return fmt.Errorf("DefaultService.RefreshGroups(%d) : %w", uid, err) - } - - groupsFormatted := make([]domain.Group, len(groups)) - for i, group := range groups { - groupIdStr := group.GroupId - groupIdInt, err := strconv.Atoi(groupIdStr) - - if err != nil { - return fmt.Errorf("DefaultService.RefreshGroups(%d) : %w", uid, err) - } - - groupsFormatted[i] = domain.Group{ - GroupID: groupIdInt, - Title: group.Title, - TimeLesson: getTime(group.TimeLesson), - } - } - - if len(groupsFormatted) == 0 { - return fmt.Errorf("DefaultService.RefreshGroups(%d) : %w", uid, appError.ErrHasNone) - } - err = d.domain.SetGroups(uid, groupsFormatted) - if err != nil { - return fmt.Errorf("DefaultService.RefreshGroups(%d) : %w", uid, err) - } - - return nil -} - -func (d DefaultService) RegisterUser(uid int64) error { - err := d.domain.RegisterUser(uid) - if err != nil { - return fmt.Errorf("DefaultService.RegisterUser(%d) : %w", uid, err) - } - return nil -} -func (d DefaultService) IsUserRegistered(uid int64) (bool, error) { - _, err := d.domain.User(uid) - if err != nil { - if errors.Is(err, appError.ErrNotValid) { - return false, nil - } - return false, fmt.Errorf("DefaultService.IsUserRegistered(%d) : %w", uid, err) - } - return true, nil -} - -func matchDates(timeStr string, t time.Time) bool { - timeStr = regexp.MustCompile(`^[а-яА-Я]+(\s+)?`).ReplaceAllString(timeStr, "") - timeFormatted, err := time.Parse("02.01.06 15:04", timeStr) - if err != nil { - log.Printf("Cant convert date str to Time - '%s'\n", timeStr) - return false - } - - if t.YearDay() == timeFormatted.YearDay() { - return true - } - return false -} -func getTime(lesson string) time.Time { - parse, err := time.Parse("02.01.2006 15:04", lesson) - if err != nil { - return time.Time{} - } - return parse -} diff --git a/internal_old/service/service.go b/internal_old/service/service.go deleted file mode 100644 index 003538e..0000000 --- a/internal_old/service/service.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "algobot/internal_old/models" - "time" -) - -type Service interface { - CurrentGroup(uid int64, t time.Time) (models.Group, error) - Groups(uid int64) ([]models.Group, error) - Cookie(uid int64) (string, error) - SetCookie(uid int64, cookie string) error - Notification(uid int64) (bool, error) - SetNotification(uid int64, notification bool) error - IsUserRegistered(uid int64) (bool, error) - RegisterUser(uid int64) error - RefreshGroups(uid int64) error - ActualInformation(uid int64, t time.Time, groupId int) (models.ActualInformation, error) - AllKidsNames(uid int64, groupId int) (models.AllKids, error) - OpenLesson(uid int64, lessonId int, groupId int) error - CloseLesson(uid int64, lessonId int, groupId int) error - AllCredentials(uid int64, groupId int) (map[string]string, error) - UsersByNotif(status bool) ([]models.ScheduleData, error) - NewMessageByUID(uid int64) ([]models.Message, error) - FullGroupInfo(uid int64, groupId int) (models.FullGroupInfo, error) - FullKidInfo(uid int64, kidID int, groupId int) (models.FullKidInfo, error) -} diff --git a/internal_old/stateMachine/memory.go b/internal_old/stateMachine/memory.go deleted file mode 100644 index af63951..0000000 --- a/internal_old/stateMachine/memory.go +++ /dev/null @@ -1,31 +0,0 @@ -package stateMachine - -import "sync" - -type Memory struct { - mu sync.Mutex - statements map[int64]Statement -} - -func NewMemory() *Memory { - return &Memory{ - statements: make(map[int64]Statement), - } -} -func (m *Memory) SetStatement(uid int64, statement Statement) { - m.mu.Lock() - defer m.mu.Unlock() - - m.statements[uid] = statement -} -func (m *Memory) GetStatement(uid int64) Statement { - m.mu.Lock() - defer m.mu.Unlock() - - v, ok := m.statements[uid] - if ok { - return v - } - m.statements[uid] = Default - return m.statements[uid] -} diff --git a/internal_old/stateMachine/stateMachine.go b/internal_old/stateMachine/stateMachine.go deleted file mode 100644 index 0a77de9..0000000 --- a/internal_old/stateMachine/stateMachine.go +++ /dev/null @@ -1,18 +0,0 @@ -package stateMachine - -type Statement string - -const ( - Default Statement = "default" - SendingCookie Statement = "sendingCookie" - ChattingAI Statement = "chattingAI" -) - -func (s Statement) String() string { - return string(s) -} - -type StateMachine interface { - GetStatement(uid int64) Statement - SetStatement(uid int64, statement Statement) -} diff --git a/test_v2/lib/fsm/memory_test.go b/test/lib/fsm/memory_test.go similarity index 100% rename from test_v2/lib/fsm/memory_test.go rename to test/lib/fsm/memory_test.go diff --git a/test/mocks/mockgen.go b/test/mocks/mockgen.go new file mode 100644 index 0000000..f726b26 --- /dev/null +++ b/test/mocks/mockgen.go @@ -0,0 +1 @@ +package mocks diff --git a/test_v2/mocks/slog_mock.go b/test/mocks/slog_mock.go similarity index 100% rename from test_v2/mocks/slog_mock.go rename to test/mocks/slog_mock.go diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go new file mode 100644 index 0000000..44dab10 --- /dev/null +++ b/test/mocks/telegram/handlers/mockgen.go @@ -0,0 +1,3 @@ +package mocks + +//go:generate mockgen -destination=./set_stater_mock.go -package=mocks algobot/internal/telegram/handlers/text SetStater diff --git a/test/mocks/telegram/mockgen.go b/test/mocks/telegram/mockgen.go new file mode 100644 index 0000000..0d3dbc7 --- /dev/null +++ b/test/mocks/telegram/mockgen.go @@ -0,0 +1,3 @@ +package mocks + +//go:generate mockgen -destination=./context_mock.go -package=mocks gopkg.in/telebot.v4 Context diff --git a/test/telegram/handlers/start_test.go b/test/telegram/handlers/start_test.go new file mode 100644 index 0000000..7509129 --- /dev/null +++ b/test/telegram/handlers/start_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/telegram/handlers/text" + mocks2 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" +) + +func TestStart(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stater := mocks.NewMockSetStater(ctrl) + mctx := mocks2.NewMockContext(ctrl) + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&telebot.User{ID: 1}).Times(1), + stater.EXPECT().SetState(int64(1), fsm.Default).Times(1), + mctx.EXPECT().Send("Открыто главное меню:", keyboards.Start()).Return(nil).Times(1), + ) + + err := text.NewStart(stater)(mctx) + assert.NoError(t, err) +} diff --git a/test_v2/mocks/handler_mock.go b/test_v2/mocks/handler_mock.go deleted file mode 100644 index e17b8a9..0000000 --- a/test_v2/mocks/handler_mock.go +++ /dev/null @@ -1,55 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: algobot/internal/telegram/dispatcher/text (interfaces: Handler) -// -// Generated by this command: -// -// mockgen -destination=./handler_mock.go -package=mocks algobot/internal/telegram/dispatcher/text Handler -// - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "go.uber.org/mock/gomock" - telebot_v4 "gopkg.in/telebot.v4" -) - -// MockHandler is a mock of Handler interface. -type MockHandler struct { - ctrl *gomock.Controller - recorder *MockHandlerMockRecorder - isgomock struct{} -} - -// MockHandlerMockRecorder is the mock recorder for MockHandler. -type MockHandlerMockRecorder struct { - mock *MockHandler -} - -// NewMockHandler creates a new mock instance. -func NewMockHandler(ctrl *gomock.Controller) *MockHandler { - mock := &MockHandler{ctrl: ctrl} - mock.recorder = &MockHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockHandler) EXPECT() *MockHandlerMockRecorder { - return m.recorder -} - -// Handle mocks base method. -func (m *MockHandler) Handle(c telebot_v4.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Handle", c) - ret0, _ := ret[0].(error) - return ret0 -} - -// Handle indicates an expected call of Handle. -func (mr *MockHandlerMockRecorder) Handle(c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockHandler)(nil).Handle), c) -} diff --git a/test_v2/mocks/mockgen.go b/test_v2/mocks/mockgen.go deleted file mode 100644 index 69d1ab3..0000000 --- a/test_v2/mocks/mockgen.go +++ /dev/null @@ -1,4 +0,0 @@ -package mocks - -// Dispatcher handler -//go:generate mockgen -destination=./handler_mock.go -package=mocks algobot/internal/telegram/dispatcher/text Handler diff --git a/test_v2/telegram/dispatcher/dispatcher_test.go b/test_v2/telegram/dispatcher/dispatcher_test.go deleted file mode 100644 index b5ecd86..0000000 --- a/test_v2/telegram/dispatcher/dispatcher_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package test - -import ( - "algobot/internal/lib/fsm" - "algobot/internal/telegram/dispatcher/text" - "algobot/test_v2/mocks" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - "testing" -) - -func Test_Dispatcher(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - h1 := mocks.NewMockHandler(ctrl) - h2 := mocks.NewMockHandler(ctrl) - h3 := mocks.NewMockHandler(ctrl) - - log := mocks.NewMockLogger() - - dispather := text.NewDispatcher(log) - dispather.Register(fsm.Default, h1) - dispather.Register(fsm.SendingCookie, h2) - dispather.Register(fsm.ChattingAI, h3) - - handler := dispather.GetHandlers(fsm.Default) - assert.Same(t, h1, handler) - handler = dispather.GetHandlers(fsm.SendingCookie) - assert.Same(t, h2, handler) - handler = dispather.GetHandlers(fsm.ChattingAI) - assert.Same(t, h3, handler) - -} diff --git a/tests/clients/backoffice_test.go b/tests/clients/backoffice_test.go deleted file mode 100644 index f08b3a3..0000000 --- a/tests/clients/backoffice_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package test - -import ( - "algobot/internal_old/clients" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "reflect" - "testing" - "time" -) - -func TestBackoffice(t *testing.T) { - boSettings := clients.BackofficeSetting{ - Retry: 3, - Timeout: 50 * time.Millisecond, - RetryTimeout: 50 * time.Millisecond, - } - - t.Run("GetKidsNamesByGroup", func(t *testing.T) { - t.Run("GENERAL | 401 | Unauthorized", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("[]")) - })) - - bo := clients.NewBackoffice(ts.URL, boSettings) - _, err := bo.GetKidsNamesByGroup("", 1) - fmt.Fprintf(os.Stdout, "%+v\n", err) - assertError(t, err, "Backoffice.GetKidsNamesByGroup(, %!s(int=1)) : Backoffice.doReq() : not found : 401 Unauthorized []") - }) - t.Run("GENERAL | 500 | Servers returns error", func(t *testing.T) { - var calls []string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - calls = append(calls, r.Method) - })) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - _, err := bo.GetKidsNamesByGroup("", 1) - assertError(t, err, "Backoffice.GetKidsNamesByGroup(, %!s(int=1)) : Backoffice.doReq() : 500 Internal Server Error ") - if len(calls) != 3 { - t.Fatalf("expected 3 calls, got %d", len(calls)) - } - - }) - t.Run("GENERAL | Timeout | Servers return timeout", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - })) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - _, err := bo.GetKidsNamesByGroup("", 1) - if !errors.Is(err, context.DeadlineExceeded) { - t.Errorf("Wanted %s, got %s", context.DeadlineExceeded, err.Error()) - } - }) - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - groupId := "333" - - ts := getBOServer(t, map[string]string{ - "groupId": groupId, - "expand": "lastGroup, groups", - }, "GET", cookie, "", GetKidsNamesByGroupResponse) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - kids, err := bo.GetKidsNamesByGroup(cookie, 333) - assertNoError(t, err) - - ks := clients.GroupResponse{} - json.Unmarshal([]byte(GetKidsNamesByGroupResponse), &ks) - if reflect.DeepEqual(ks, kids) { - t.Errorf("Wanted: %+v", ks) - t.Errorf("Got: %+v", kids) - t.Fatalf("expected kids to be different") - } - }) - }) - t.Run("GetKidsStatsByGroup", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - groupId := "333" - - ts := getBOServer(t, map[string]string{ - "group": groupId, - }, "GET", cookie, "", GetKidsStatsByGroupResponse) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - kids, err := bo.GetKidsStatsByGroup(cookie, groupId) - assertNoError(t, err) - - ks := clients.KidsStats{} - json.Unmarshal([]byte(GetKidsStatsByGroupResponse), &ks) - if reflect.DeepEqual(ks, kids) { - t.Errorf("Wanted: %+v", ks) - t.Errorf("Got: %+v", kids) - t.Fatalf("expected kids to be different") - } - }) - }) - t.Run("GetKidsMessages", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - ts := getBOServer(t, map[string]string{ - "from": "0", - "limit": "30", - }, "GET", cookie, "", KidsMessagesResponse) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - kids, err := bo.GetKidsMessages(cookie) - assertNoError(t, err) - - ks := clients.KidsStats{} - json.Unmarshal([]byte(KidsMessagesResponse), &ks) - if reflect.DeepEqual(ks, kids) { - t.Errorf("Wanted: %+v", ks) - t.Errorf("Got: %+v", kids) - t.Fatalf("expected kids to be different") - } - }) - }) - t.Run("GetAllGroupsByUser", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - ts := getBOServer(t, map[string]string{ - "GroupSearch[status][]": "active", - "presetType": "all", - "_pjax": "#group-grid-pjax", - }, "GET", cookie, "", HtmlResponse) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - kids, err := bo.GetAllGroupsByUser(cookie) - assertNoError(t, err) - - if !reflect.DeepEqual(Kids, kids) { - t.Errorf("Wanted: %+v", Kids) - t.Fatalf("Got: %+v", kids) - } - }) - }) - t.Run("CloseLession", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - group := "111" - lession := "222" - ts := getBOServer(t, map[string]string{}, "POST", cookie, `ajaxUrl=%2Fapi%2Fv2%2Fgroup%2Flesson%2Fstatus&btnClass=btn+btn-xs+btn-danger&groupId=111&lessonId=222&status=0`, "[]") - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - err := bo.CloseLession(cookie, group, lession) - assertNoError(t, err) - }) - }) - t.Run("OpenLession", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - group := "111" - lession := "222" - ts := getBOServer(t, map[string]string{}, "POST", cookie, `ajaxUrl=%2Fapi%2Fv2%2Fgroup%2Flesson%2Fstatus&btnClass=btn+btn-xs+btn-danger&groupId=111&lessonId=222&status=10`, "[]") - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - err := bo.OpenLession(cookie, group, lession) - assertNoError(t, err) - }) - }) - t.Run("GetGroupInfo", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - ts := getBOServer(t, map[string]string{ - "expand": "venue,teacher,curator,branch", - }, "GET", cookie, "", FullGroupInfo) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - info, err := bo.GetGroupInfo(cookie, "1") - assertNoError(t, err) - - if info.Data.Title != "Библиотека 10 пн 16.00" { - t.Fatalf("Wanted: %s, got %s", "Библиотека 10 пн 16.00", info.Data.Title) - } - if info.Data.Branch.Email != "moscow@example.org" { - t.Fatalf("Wanted: %s, got %s", "moscow@example.org", info.Data.Branch.Email) - } - }) - }) - t.Run("GetKidInfo", func(t *testing.T) { - t.Run("200 | Servers return OK", func(t *testing.T) { - cookie := "111" - ts := getBOServer(t, map[string]string{ - "expand": "groups", - }, "GET", cookie, "", FullKidInfo) - defer ts.Close() - - bo := clients.NewBackoffice(ts.URL, boSettings) - info, err := bo.GetKidInfo(cookie, "1") - assertNoError(t, err) - - if info.Data.FullName != "Иван Иванов" { - t.Fatalf("Wanted: %s, got %s", "Иван Иванов", info.Data.FullName) - } - if info.Data.Email != "ivanov-maria@example.com" { - t.Fatalf("Wanted: %s, got %s", "ivanov-maria@example.com", info.Data.Email) - } - }) - }) -} - -func getBOServer(t *testing.T, wantedParams map[string]string, wantedMethod string, wantedCookie string, wantedBody string, serverResponse string) *httptest.Server { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - uri, _ := url.Parse(r.RequestURI) - params := uri.Query() - - for k, v := range wantedParams { - if params.Get(k) != v { - t.Fatalf("expected %s=%s, got %s", k, v, params.Get(k)) - } - } - if r.Method != wantedMethod { - w.WriteHeader(http.StatusBadRequest) - return - } - if r.Header.Get("Cookie") != wantedCookie { - w.WriteHeader(http.StatusUnauthorized) - return - } - if getString(r.Body) != wantedBody { - w.WriteHeader(http.StatusBadRequest) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte(serverResponse)) - })) - return ts -} -func assertNoError(t *testing.T, err error) { - t.Helper() - - if err != nil { - t.Fatal(err) - } -} -func assertError(t *testing.T, err error, s string) { - t.Helper() - - if err.Error() != s { - t.Errorf("Wanted %s, got %s", s, err.Error()) - } -} -func getString(body io.Reader) string { - all, err := io.ReadAll(body) - if err != nil { - return "" - } - return string(all) -} - -const GetKidsNamesByGroupResponse = `{"status": "success","data": {"items": [{"id": 70245813,"firstName": "firstName","lastName": "lastName","fullName": "firstName lastName","parentName": "parentName","email": "email@mail.ru","hasLaptop": -1,"phone": "+7 (999) 999-99-99","age": 10,"birthDate": "2014-12-16T00:00:00+03:00","createdAt": "2024-11-18T08:18:45+00:00","updatedAt": "2024-12-03T08:31:26+00:00","deletedAt": null,"hasBranchAccess": true,"username": "username","password": "password","lastGroup": {"id": 98637162,"groupStudentId": 6553709,"title": "title","content": "content","track": 2,"status": 0,"startTime": "2024-11-25T10:54:55+03:00","endTime": "9999-12-31T00:00:00+03:00","courseId": 729,"createdAt": "2024-09-17T07:41:22+00:00","updatedAt": "2025-01-15T09:10:46+00:00","deletedAt": null},"_links": {"self": {"href": "/student/update/70245813"}}}]}}` -const GetKidsStatsByGroupResponse = `{"status": "success","data": [{"student_id": 3356212,"attendance": [{"lesson_id": 4560,"lesson_title": "1","start_time_formatted": "вс 22.09.24 14:00","status": "absent"},{"lesson_id": 4561,"lesson_title": "2","start_time_formatted": "вс 29.09.24 14:00","status": "absent"}]}]}` -const KidsMessagesResponse = `{"status": "success","data": {"projects": [{"uid": "1039632level154551","new": false,"senderId": 1039632,"senderScope": "student","type": "text","content": "11","name": "a a","lastTime": "11 янв. 15:25","title": "Что такое w-w w?","link": "/task-preview/16402?student=s&lesson=s&position=5&s=1&groupId=98619873"}]}}` -const HtmlResponse = `Группы

Группы

Записей на странице
IDНазваниеПлощадкаУч-ки ЗачисленныеВремя след. урока След. урокСлед. урокПреподавательКураторТип группыСтатусФормат
 
 
 
 
98637162Библиотека 7 вс 14.00

Группа по курсу КГ

Библиотека №79 (0)019.01.2025 14:0017Знакомство с презентациями
КГ М5У1
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98623409Библиотека 7 сб 16.00

Группа по курсу ОЛиП МП

Библиотека №75 (0)018.01.2025 16:0018Урок 18. Использование сообщений в игре
М5У2, курс "Основы логики и программирования", 2021-2022
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98623404Библиотека 7 вс 12.00

Группа по курсу ОЛиП МП

Библиотека №76 (0)019.01.2025 12:0019Урок 18. Использование сообщений в игре
М5У2, курс "Основы логики и программирования", 2021-2022
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98621252Библиотека 7 вс 18.00

Группа по курсу Пст

Библиотека №79 (0)019.01.2025 18:0018М5 У1. ООП. Объекты и методы
Python Start 2021/2022 М5 У1
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98619913Библиотека № 7 сб 10.00

Группа по курсу КГ

Библиотека №79 (0)018.01.2025 10:0018Структурируем презентацию
КГ М5У2
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98619873Библиотека № 7 сб 14.00

Группа по курсу ГД

Библиотека №78 (0)018.01.2025 14:0018Добавление персонажей в игру
ГД 21/22 М4У2
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98619867Библиотека № 7 сб 12.00

Группа по курсу ВП

Библиотека №79 (0)018.01.2025 12:0018М4У2. Цикл с условием
Визуальное программирование, М4У2
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
98589447Библиотека № 7 вс 10.00

Группа по курсу ВП

Библиотека №78 (0)019.01.2025 10:0019М4У2. Цикл с условием
Визуальное программирование, М4У2
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
985504Библиотека 7 сб 18.00

Группа по курсу Пст 2

Библиотека №76 (0)018.01.2025 18:0019М4 У2. Приложение Easy Editor. Ч.1
Python Start - 2 2021/2022 М4 У2
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
978298Библиотека 7 вс 16.00

Группа по курсу Пст 2

Библиотека №77 (0)019.01.2025 16:0019М4 У4. Приложение Easy Editor. Ч. 3
Python Start - 2 2021/2022 М4 У4
Данил ПавловМаксим КозловГруппаАктивная
Офлайн
` -const FullGroupInfo = `{"status": "success","data": {"id": 12345678,"title": "Библиотека 10 пн 16.00","content": "Группа по курсу Программирование","type": {"value": "regular","label": "Группа","tag": "default"},"status": {"value": 10,"label": "Активная","tag": "success"},"status_changed_at": "25.10.2024 10:15","start_time": "28.10.2024 16:00","next_lesson_time": "15.03.2025 16:00","lessons_total": 24,"lessons_passed": 12,"hardware_needed": 1,"branch": {"id": 45,"title": "Москва","code": "moscow","description": "","phone": "+7 (495) 123-45-67","email": "moscow@example.org","site_url": "https://moscow.example.org","templateVersion": 1,"use_amo": true,"amoConfigId": 123,"show_finance_info": true,"lms_display_student_credentials": true,"show_online_room_url_field": 0,"use_sms": false,"language_id": 2,"order_name": 1,"use_fully_paid_label": 0,"brandName": "","max_count_students_for_show_online": 15,"isFillPaymentSystem": false,"firstLessonNoRoyalty": 0,"root_branch_id": 50},"venue": {"id": 9876,"title": "Библиотека №10","address": "123456, Москва, ул. Примерная, д. 15 к. 3","contact_name": "","contact_email": "","contact_phone": "","_links": {"self": "/venue/view/9876","update": "/venue/update/9876","index": "/venue"}},"curator": {"id": 7890,"username": "Ivan_I","phone": "+7 (999) 888-77-66","email": "ivan@example.com","name": "Иван Иванов","profile": {"photo_url": "/uploads/avatar/avatar/avatar_7890_1603094230-96x96.jpg","promo": ""},"allowedUserCourses": [],"status": 10,"_links": {"self": "/user/update/7890"}},"teacher": {"id": 54321,"username": "A_Petrov","phone": "+7 (987) 654-32-10","email": "alex@example.com","name": "Алексей Петров","profile": {"photo_url": "","promo": null},"allowedUserCourses": [{"userId": 54321,"courseId": 123,"isAllowed": 1}],"status": 10,"_links": {"self": "/user/update/54321"}},"teachers": [{"id": 54321,"username": "A_Petrov","phone": "+7 (987) 654-32-10","email": "alex@example.com","name": "Алексей Петров","profile": {"photo_url": "","promo": null},"allowedUserCourses": [{"userId": 54321,"courseId": 123,"isAllowed": 1}],"status": 10,"_links": {"self": "/user/update/54321"}}],"client_manager": null,"course": {"id": 456,"name": "Основы программирования","guid": "b242345b-cde5-11eb-a724-6cb31107bf10","description": "Курс для начинающих программистов, на русском языке, версия 2023/2024","contentType": "course","courseType": {"id": 20,"title": "программирование","code": "prog"},"lessons_count": 0,"group_lessons_amount": 0,"lessons_count_formatted": "нет модулей","group_lessons_amount_formatted": "нет уроков","is_deleted": 0,"_links": {"self": "/course/view/b242345b-cde5-11eb-a724-6cb31107bf10"}},"language_id": null,"journal": true,"show_journal": true,"showOnlineRoom": true,"isOnline": false,"active_student_count": 15,"online_room_url": "","use_client_manager": 0,"display_lesson_duration_in_minutes": 90,"deleted_at": null,"deleted_by": null,"priority_level": {"value": "normal","label": "Обычный приоритет","tag": "default"},"is_full": false,"created_at": "20.10.2024 09:30","created_by": {"id": 7890,"username": "Ivan_I","phone": "+7 (999) 888-77-66","email": "ivan@example.com","name": "Иван Иванов","profile": {"photo_url": "/uploads/avatar/avatar/avatar_7890_1603094230-96x96.jpg","promo": ""},"allowedUserCourses": [],"status": 10,"_links": {"self": "/user/update/7890"}},"_related": {"statuses": [{"value": 10,"label": "Активная","tag": "success"}],"types": [{"value": "regular","label": "Группа","tag": "default"}],"priority_levels": [{"value": "normal","label": "Обычный","tag": "default"}]}}}` -const FullKidInfo = `{"status": "success","data": {"id": 1234567,"firstName": "Иван","lastName": "Иванов","fullName": "Иван Иванов","parentName": "Мария","email": "ivanov-maria@example.com","hasLaptop": 1,"phone": "+7 (800) 123-45-67","age": 22,"birthDate": "1995-07-15T00:00:00+03:00","createdAt": "2022-05-01T12:30:00+00:00","updatedAt": "2024-01-20T18:45:30+00:00","deletedAt": null,"hasBranchAccess": false,"username": "ivanov123","password": "password123","groups": [{"id": 999999,"groupStudentId": 1234567,"title": "Группа по курсу 'Математика 101'","content": "Образовательная группа по основам математики","track": 2,"status": 1,"startTime": "2023-06-01T10:00:00+03:00","endTime": "2025-06-01T00:00:00+03:00","courseId": 101,"createdAt": "2023-04-10T15:15:30+00:00","updatedAt": "2024-01-15T14:00:10+00:00","deletedAt": null}],"_links": {"self": {"href": "/student/update/1234567"}}}}` - -var Kids = []clients.AllGroupsUser{ - {Title: "Группа по курсу КГ", GroupId: "98637162", TimeLesson: "19.01.2025 14:00", RegularTime: "Библиотека 7 вс 14.00"}, - {Title: "Группа по курсу ОЛиП МП", GroupId: "98623409", TimeLesson: "18.01.2025 16:00", RegularTime: "Библиотека 7 сб 16.00"}, - {Title: "Группа по курсу ОЛиП МП", GroupId: "98623404", TimeLesson: "19.01.2025 12:00", RegularTime: "Библиотека 7 вс 12.00"}, - {Title: "Группа по курсу Пст", GroupId: "98621252", TimeLesson: "19.01.2025 18:00", RegularTime: "Библиотека 7 вс 18.00"}, - {Title: "Группа по курсу КГ", GroupId: "98619913", TimeLesson: "18.01.2025 10:00", RegularTime: "Библиотека № 7 сб 10.00"}, - {Title: "Группа по курсу ГД", GroupId: "98619873", TimeLesson: "18.01.2025 14:00", RegularTime: "Библиотека № 7 сб 14.00"}, - {Title: "Группа по курсу ВП", GroupId: "98619867", TimeLesson: "18.01.2025 12:00", RegularTime: "Библиотека № 7 сб 12.00"}, - {Title: "Группа по курсу ВП", GroupId: "98589447", TimeLesson: "19.01.2025 10:00", RegularTime: "Библиотека № 7 вс 10.00"}, - {Title: "Группа по курсу Пст 2", GroupId: "985504", TimeLesson: "18.01.2025 18:00", RegularTime: "Библиотека 7 сб 18.00"}, - {Title: "Группа по курсу Пст 2", GroupId: "978298", TimeLesson: "19.01.2025 16:00", RegularTime: "Библиотека 7 вс 16.00"}, -} diff --git a/tests/domain/domain_test.go b/tests/domain/domain_test.go deleted file mode 100644 index a1f3609..0000000 --- a/tests/domain/domain_test.go +++ /dev/null @@ -1,306 +0,0 @@ -package domain - -import ( - "algobot/internal_old/domain" - appError "algobot/internal_old/error" - "database/sql" - "errors" - "fmt" - _ "github.com/ncruces/go-sqlite3/driver" - _ "github.com/ncruces/go-sqlite3/embed" - "log" - "os" - "reflect" - "testing" - "time" -) - -func TestDomain(t *testing.T) { - const baseName = "temp.db" - base, close := getSqliteBase(baseName) - defer cleanup(baseName, close) - - sqlite3 := domain.NewSqlite3(base) - sqlite3.Migrate(os.DirFS("../../cmd"), "migrations") - - t.Run("Test User method", func(t *testing.T) { - t.Run("When user is not created", func(t *testing.T) { - truncateBase(base) - - user, err := sqlite3.User(1) - if err == nil { - fmt.Printf("%#v\n", user) - t.Fatal("Expected error, got nil") - } - }) - t.Run("When user is created", func(t *testing.T) { - truncateBase(base) - - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(2, 'agent', 'cookie', 0);") - base.Exec("INSERT INTO groups (group_id, owner_id, title, time_lesson) VALUES(0, ?, 'title', '2025-02-01 16:00:00');", 2) - - user, err := sqlite3.User(2) - if err != nil { - fmt.Printf("%s\n", err) - t.Fatal("Expected user, got error") - } - - assertUser(t, user, "agent", "cookie", false) - assertGroups( - t, - user.Groups, - []domain.Group{ - { - GroupID: 0, - Title: "title", - TimeLesson: time.Date(2025, 2, 1, 16, 0, 0, 0, time.UTC), - }, - }, - ) - }) - }) - t.Run("Test cookie method", func(t *testing.T) { - t.Run("Cookie not exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(3, 'agent', NULL, 0);") - _, err := sqlite3.Cookie(3) - if err == nil { - t.Fatalf("Expected error, got nil") - } - }) - t.Run("Cookie exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(4, 'agent', 'cookie', 0);") - cookie, err := sqlite3.Cookie(4) - if err != nil { - t.Error(err) - t.Fatalf("Expected cookie, got error") - } - if cookie != "cookie" { - t.Errorf("Expected cookie to be 'cookie', got %s", cookie) - } - }) - }) - t.Run("Test set cookie method", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(5, 'agent', 'a', 0);") - sqlite3.SetCookie(5, "cookie") - cookie, _ := sqlite3.Cookie(5) - - if cookie != "cookie" { - t.Errorf("Expected cookie to be 'cookie', got %s", cookie) - } - }) - t.Run("Test set userAgent", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(6, 'a', 'a', 0);") - sqlite3.SetUserAgent(6, "agent") - - row := base.QueryRow("SELECT u.user_agent FROM users u WHERE u.uid = ?", 6) - - var userAgent string - row.Scan(&userAgent) - - if userAgent != "agent" { - t.Errorf("Expected userAgent to be 'agent', got %s", userAgent) - } - }) - t.Run("Test Groups", func(t *testing.T) { - t.Run("Get groups", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(7, 'agent', 'cookie', 0);") - base.Exec("INSERT INTO groups (group_id, owner_id, title, time_lesson) VALUES(0, ?, 'title', '2025-02-01 16:00:00');", 7) - - groups, err := sqlite3.Groups(7) - if err != nil { - t.Fatal(err) - } - - assertGroups( - t, - groups, - []domain.Group{ - { - GroupID: 0, - Title: "title", - - TimeLesson: time.Date(2025, 2, 1, 16, 0, 0, 0, time.UTC), - }, - }, - ) - }) - t.Run("Set groups", func(t *testing.T) { - truncateBase(base) - wanted := []domain.Group{ - { - GroupID: 0, - Title: "title", - - TimeLesson: time.Date(2025, 2, 1, 16, 0, 0, 0, time.UTC), - }, - } - - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(8, 'agent', 'cookie', 0);") - sqlite3.SetGroups(8, wanted) - - groups, err := sqlite3.Groups(8) - if err != nil { - t.Fatal(err) - } - - assertGroups( - t, - wanted, - groups, - ) - }) - }) - t.Run("Test notification method", func(t *testing.T) { - t.Run("notification not exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(3, 'agent', NULL, NULL);") - notif, _ := sqlite3.Notification(3) - if notif != false { - t.Fatalf("Expected notification to be false, got true") - } - }) - t.Run("notification exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(4, 'agent', 'cookie', 1);") - notif, _ := sqlite3.Notification(4) - if notif != true { - t.Fatalf("Expected notification to be true, got false") - } - }) - }) - t.Run("Test set notification method", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, notification) VALUES(5, 'agent', 'a', 0);") - sqlite3.SetNotification(5, true) - notif, _ := sqlite3.Notification(5) - - if notif != true { - t.Errorf("Expected notif to be 'true', got %v", notif) - } - }) - t.Run("Register user ", func(t *testing.T) { - truncateBase(base) - - sqlite3.RegisterUser(1) - - var exists bool - base.QueryRow("SELECT EXISTS (SELECT 1 FROM users where uid = ?)", 1).Scan(&exists) - if exists == false { - t.Fatalf("User does not exist") - } - }) - t.Run("NotificationDate", func(t *testing.T) { - t.Run("Get if not exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(14, 'agent', 'cookie', '', 0);") - _, err := sqlite3.LastNotificationDate(14) - if err == nil { - if errors.Is(err, appError.ErrNotValid) { - t.Errorf("Wanted errNotValid, got %v", err) - } - t.Errorf("Wanted error, got nothing") - } - }) - t.Run("Get if exists", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(14, 'agent', 'cookie', 'last_notification_msg', 0);") - notif, err := sqlite3.LastNotificationDate(14) - if err != nil { - t.Errorf("Wanted result, got error") - } - if notif != "last_notification_msg" { - t.Errorf("Wanted last_notification_msg, got %v", notif) - } - }) - t.Run("Set", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(14, 'agent', 'cookie', 'last_notification_msg', 0);") - err := sqlite3.SetLastNotificationDate(14, "asd") - if err != nil { - t.Errorf("Wanted change, got error") - } - notif, err := sqlite3.LastNotificationDate(14) - if err != nil { - t.Errorf("Wanted result, got error") - } - if notif != "asd" { - t.Errorf("Wanted asd, got %v", notif) - } - }) - }) - t.Run("GetUsersByNotification", func(t *testing.T) { - truncateBase(base) - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(14, 'agent', 'cookie', '', 0);") - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(15, 'agent', 'cookie', '', 1);") - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(16, 'agent', 'cookie', '', 0);") - base.Exec("INSERT INTO users (uid, user_agent, cookie, last_notification_msg, notification) VALUES(17, 'agent', 'cookie', '', 1);") - - users, err := sqlite3.GetUsersByNotification(1) - if err != nil { - t.Fatal(err) - } - if len(users) != 2 { - t.Errorf("Expected 2 users, got %v", len(users)) - } - if users[0].UID != 15 || users[1].UID != 17 { - t.Errorf("Expected 15, 17 UID user, got %v, %v", users[0].UID, users[0].UID) - } - }) -} - -func truncateBase(base *sql.DB) { - base.Exec("DELETE FROM users;") - base.Exec("DELETE FROM groups;") -} - -func assertGroups(t *testing.T, groups []domain.Group, groups2 []domain.Group) { - t.Helper() - - if !reflect.DeepEqual(groups, groups2) { - t.Fatalf("Wanted equals, got %#v - %#v", groups, groups2) - } -} - -func assertUser(t *testing.T, user domain.User, userAgent string, cookie string, notif bool) { - t.Helper() - - if user.UserAgent != userAgent { - t.Fatalf("Expected userAgent to be %s, got %s", user.UserAgent, userAgent) - } - if user.Notifications != notif { - t.Fatalf("Expected notifications to be %v, got %v", notif, user.Notifications) - } - if user.Cookie != cookie { - t.Fatalf("Expected cookie to be %s, got %s", user.Cookie, cookie) - } -} - -func cleanup(name string, closeBd func() error) { - if err := closeBd(); err != nil { - fmt.Printf("Ошибка при закрытии базы данных: %v\n", err) - } - - if err := os.Remove(name); err != nil { - fmt.Printf("Ошибка при удалении файла базы данных: %v\n", err) - } -} - -func getSqliteBase(name string) (*sql.DB, func() error) { - db, err := sql.Open("sqlite3", "file:"+name) - if err != nil { - log.Fatal(err) - } - - err = db.Ping() - if err != nil { - log.Println(err) - } - - log.Print("Подключение к базе данных установлено\n") - return db, db.Close -} diff --git a/tests/handlers/chattingAI/chattingAI_test.go b/tests/handlers/chattingAI/chattingAI_test.go deleted file mode 100644 index d784482..0000000 --- a/tests/handlers/chattingAI/chattingAI_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package test - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers" - "algobot/internal_old/stateMachine" - "algobot/tests/mocks" - "errors" - "github.com/golang/mock/gomock" - "gopkg.in/telebot.v4" - "reflect" - "strings" - "testing" -) - -func TestSending(t *testing.T) { - t.Run("Send back action", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockAI := mocks.NewMockAIService(ctrl) - - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.ChattingAI) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, config.BackBtn.Text) - - messageHandler.Handle(&mockContext) - - if mockState.Current != stateMachine.Default { - t.Fatalf("Wanted default got %+v\n", mockState.Current) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.StartText) - assertKeyboards(t, mockContext.SentMessages[0], config.StartKeyboard) - }) - t.Run("Send clear history action", func(t *testing.T) { - t.Run("Suuccesed", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockAI := mocks.NewMockAIService(ctrl) - - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.ChattingAI) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, config.ClearHistoryBtn.Text) - - mockAI.EXPECT().ClearAllHistory(gomock.Any()).Return(nil) - - messageHandler.Handle(&mockContext) - - assertMessages(t, mockContext.SentMessages[0], "Успешно отчищено!") - }) - t.Run("Fail", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockAI := mocks.NewMockAIService(ctrl) - - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.ChattingAI) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, config.ClearHistoryBtn.Text) - - mockAI.EXPECT().ClearAllHistory(gomock.Any()).Return(errors.New("")) - - messageHandler.Handle(&mockContext) - - assertContainsMessages(t, mockContext.SentMessages[0], "Ошибка при отчистке памяти!") - }) - }) -} -func assertContainsMessages(t *testing.T, got mocks.SentMessage, wantedText string) { - t.Helper() - - if !strings.Contains(got.What.(string), wantedText) { - t.Errorf("Wanted [%s], but got [%s]", wantedText, got.What.(string)) - } -} -func assertMessages(t *testing.T, got mocks.SentMessage, wantedText string) { - t.Helper() - - if got.What.(string) != wantedText { - t.Errorf("Wanted [%s], but got [%s]", wantedText, got.What.(string)) - } -} -func assertKeyboards(t *testing.T, got mocks.SentMessage, wantedMarkup *telebot.ReplyMarkup) { - t.Helper() - - var gotMarkup *telebot.ReplyMarkup - for _, opt := range got.Opts { - if markup, ok := opt.(*telebot.ReplyMarkup); ok { - gotMarkup = markup - break - } - } - - if !reflect.DeepEqual(gotMarkup, wantedMarkup) { - t.Errorf("Wanted keyboard [%+v],\n but got [%+v]", wantedMarkup, gotMarkup) - } -} -func assertContextOptsLen(t *testing.T, sent mocks.SentMessage, i int) { - t.Helper() - - if len(sent.Opts) != i { - t.Errorf("%+v\n", sent) - t.Errorf("Wanted context len = %d, got, %d", i, len(sent.Opts)) - } -} diff --git a/tests/handlers/defaultState/CallbackHandler_test.go b/tests/handlers/defaultState/CallbackHandler_test.go deleted file mode 100644 index 5f50959..0000000 --- a/tests/handlers/defaultState/CallbackHandler_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package test - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers" - "algobot/internal_old/stateMachine" - "algobot/tests/mocks" - "fmt" - "testing" -) - -func TestCallback(t *testing.T) { - t.Run("Set cookie", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "set_cookie") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - - queryHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.SendingCookie) - assertKeyboards(t, mockContext.SentMessages[0], config.RejectKeyboard) - - assertMockStatement(t, mockState, stateMachine.SendingCookie, 8) - }) - t.Run("Change notification", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "change_notification") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - - if ms.StubNotification != false { - t.Fatalf("Wanted notif false, got true") - } - queryHandler.Handle(&mockContext) - if ms.StubNotification != true { - t.Fatalf("Wanted notif true, got false") - } - - assertContextOptsLen(t, mockContext.SentMessages[0], 0) - assertMessages(t, mockContext.SentMessages[0], "Настройки уведомлений были изменены!") - - }) - t.Run("Refresh groups without error", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "refresh_groups") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - queryHandler.Handle(&mockContext) - - if len(mockContext.SentMessages) != 2 { - t.Fatalf("Wanted 2, got %d", len(mockContext.SentMessages)) - } - assertMessages(t, mockContext.SentMessages[0], config.UpdateStarted) - assertMessages(t, mockContext.SentMessages[1], config.UpdateEnd) - }) - t.Run("Close lesson", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "close_lesson_1_1") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - - queryHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 0) - sprintf := fmt.Sprintf("CloseLesson(%d, %d, %d)", 12, 1, 1) - if ms.Calls[0] != sprintf { - t.Errorf("Wanted %s, got %s", sprintf, ms.Calls[0]) - } - assertMessages(t, mockContext.SentMessages[0], config.SuccessfulChangeStatus) - }) - t.Run("Open lesson", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "open_lesson_1_1") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - - queryHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 0) - sprintf := fmt.Sprintf("OpenLesson(%d, %d, %d)", 12, 1, 1) - if ms.Calls[0] != sprintf { - t.Errorf("Wanted %s, got %s", sprintf, ms.Calls[0]) - } - assertMessages(t, mockContext.SentMessages[0], config.SuccessfulChangeStatus) - }) - t.Run("Get creds", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - - mockContext := mocks.MockContext{} - mockContext.SetUserMessage(12, "get_creds_1") - - queryHandler := contextHandlers.NewOnCallback(ms, &mockState) - - queryHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 0) - sprintf := fmt.Sprintf("AllCredentials(%d, %d)", 12, 1) - if ms.Calls[0] != sprintf { - t.Errorf("Wanted %s, got %s", sprintf, ms.Calls[0]) - } - assertMessages(t, mockContext.SentMessages[0], fmt.Sprintf("Учетные записи детей:\n\nВаня [van:12]")) - }) -} - -func assertMockStatement(t *testing.T, mockState mocks.MockStateMachine, wantedState stateMachine.Statement, wantedLen int) { - if mockState.Current != wantedState { - t.Errorf("Wanted %+v, got %+v", wantedState, mockState.Current) - } - if len(mockState.Calls) != wantedLen { - t.Errorf("Wanted len %d, got %d", wantedLen, len(mockState.Calls)) - } -} diff --git a/tests/handlers/defaultState/DefaultHandler_test.go b/tests/handlers/defaultState/DefaultHandler_test.go deleted file mode 100644 index dcdeb21..0000000 --- a/tests/handlers/defaultState/DefaultHandler_test.go +++ /dev/null @@ -1,509 +0,0 @@ -package test - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers" - "algobot/internal_old/contextHandlers/textHandlers/defaultState" - appError "algobot/internal_old/error" - "algobot/internal_old/models" - "algobot/internal_old/serdes" - "algobot/internal_old/stateMachine" - "algobot/tests/mocks" - "errors" - "fmt" - "github.com/golang/mock/gomock" - "gopkg.in/telebot.v4" - "os" - "reflect" - "strings" - "testing" - "time" -) - -func TestDefaultHandler(t *testing.T) { - os.Setenv("TELEGRAM_NAME", "test") - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockAI := mocks.NewMockAIService(ctrl) - - t.Run("If user is not register", func(t *testing.T) { - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, "hello world!") - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.Incorrect) - assertKeyboards(t, mockContext.SentMessages[0], config.StartKeyboard) - - }) - t.Run("If user register", func(t *testing.T) { - t.Run("Send any bullshit", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, "aezakmi") - - messageHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.Incorrect) - assertKeyboards(t, mockContext.SentMessages[0], config.StartKeyboard) - }) - t.Run("Send settings", func(t *testing.T) { - t.Run("Cookie set, notif off", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("Cookie") - mockContext.SetUserMessage(12, "Настройки") - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], fmt.Sprintf( - "%s\n\n%s%s\n%s%s", - config.Settings, - config.Cookie, - config.SetParam, - config.ChatNotifications, - config.NotSetParam, - )) - assertKeyboards(t, mockContext.SentMessages[0], config.SettingsKeyboard) - }) - t.Run("Cookie unset, notif off", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("") - mockContext.SetUserMessage(12, "Настройки") - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], fmt.Sprintf( - "%s\n\n%s%s\n%s%s", - config.Settings, - config.Cookie, - config.NotSetParam, - config.ChatNotifications, - config.NotSetParam, - )) - assertKeyboards(t, mockContext.SentMessages[0], config.SettingsKeyboard) - }) - }) - t.Run("Send get missing kids", func(t *testing.T) { - t.Run("Group exists", func(t *testing.T) { - gr := models.Group{ - GroupID: 1, - Title: "Title", - TimeLesson: getDayByTime(28, 10, 0), - } - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.Actual = models.ActualInformation{ - LessonTitle: "LTitle", - LessonId: 0, - MissingKids: []models.MissingKid{ - { - Id: 1, - Count: 0, - }, { - Id: 2, - Count: 2, - }, { - Id: 3, - Count: 1, - }, - }, - } - ms.AllNames = models.AllKids{ - 1: models.KidData{ - FullName: "vasya", - }, - 2: models.KidData{ - FullName: "petya", - }, - 3: models.KidData{ - FullName: "kirill", - }, - 4: models.KidData{ - FullName: "olga", - }, - } - ms.SetCurrentGroup(&gr) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessageWithTime(12, "Получить отсутсвующих", getUnixByDay(28, 9, 40)) - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 2) - assertMessages(t, mockContext.SentMessages[0], "Группа по курсу: Title\nЛекция: LTitle\n\nОбщее число детей: 4\nОтсутствуют: 3\n\n```Отсутствующие\nvasya\npetya (Уже 2 занятие)\nkirill\n```") - - wantedMarkup := telebot.ReplyMarkup{ResizeKeyboard: true} - wantedMarkup.Inline( - wantedMarkup.Row(wantedMarkup.Data(config.CloseLessonBtn, "close_lesson_1_0"), wantedMarkup.Data(config.OpenLessonBtn, "open_lesson_1_0")), - wantedMarkup.Row(wantedMarkup.Data(config.GetCredsBtn, "get_creds_1")), - ) - assertKeyboards(t, mockContext.SentMessages[0], &wantedMarkup) - }) - t.Run("Group Non exists", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.SetCurrentGroup(nil) - - mockContext := mocks.MockContext{} - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - mockContext.SetUserMessageWithTime(12, "Получить отсутсвующих", getUnixByDay(28, 22, 40)) - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - containtsMessages(t, mockContext.SentMessages[0], config.CurrentGroupDontFind) - }) - }) - t.Run("Send my groups", func(t *testing.T) { - t.Run("If user have groups", func(t *testing.T) { - g := []models.Group{ - { - GroupID: 1, - Title: "Гр 1", - TimeLesson: getDayByTime(28, 10, 0), - }, - { - GroupID: 3, - Title: "Гр 3", - TimeLesson: getDayByTime(27, 14, 0), - }, - { - GroupID: 2, - Title: "Гр 2", - TimeLesson: getDayByTime(21, 12, 0), - }, - { - GroupID: 4, - Title: "Гр 4", - TimeLesson: getDayByTime(27, 10, 0), - }, - } - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.SetGroups(g) - - mockContext := mocks.MockContext{} - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, "Мои группы") - - messageHandler.Handle(&mockContext) - - assertContextOptsLen(t, mockContext.SentMessages[0], 3) - assertMessages(t, mockContext.SentMessages[0], fmt.Sprintf( - "%s4\n\n%s\n\n%s", - config.MyGroups, - "1. [Гр 4](t.me/test?start=00ybm5WSwV3bydEdldG) 🕐 сб 10:00\n2. [Гр 3](t.me/test?start=z0ybm5WSwV3bydEdldG) 🕐 сб 14:00", - "1. [Гр 1](t.me/test?start=x0ybm5WSwV3bydEdldG) 🕐 вс 10:00\n2. [Гр 2](t.me/test?start=y0ybm5WSwV3bydEdldG) 🕐 вс 12:00", - )) - assertKeyboards(t, mockContext.SentMessages[0], config.MyGroupsKeyboard) - }) - t.Run("If user dont have groups", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.SetGroupsErr(appError.ErrHasNone) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - mockContext.SetUserMessage(12, "Мои группы") - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.UserDontHaveGroup) - assertKeyboards(t, mockContext.SentMessages[0], config.MyGroupsKeyboard) - }) - }) - t.Run("Send /start with payload", func(t *testing.T) { - t.Run("Get group", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("Cookie") - payload := serdes.Serialize(models.StartPayload{ - Action: models.GetGroupInfo, - Payload: []string{"1"}, - }) - mockContext.SetPayload(payload) - mockContext.SetUserMessage(12, "/start="+payload) - - messageHandler.Handle(&mockContext) - - if ms.Calls[0] != "121" { - t.Errorf("Wanted 121, got %s", ms.Calls[0]) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 2) - assertMessages(t, mockContext.SentMessages[0], defaultState.GetGroupInfoMessage(mocks.FullGrInfo)) - }) - t.Run("Get student", func(t *testing.T) { - t.Run("If student present", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("Cookie") - payload := serdes.Serialize(models.StartPayload{ - Action: models.GetKidInfo, - Payload: []string{"1", "2"}, - }) - mockContext.SetPayload(payload) - mockContext.SetUserMessage(12, "/start="+payload) - - messageHandler.Handle(&mockContext) - if ms.Calls[0] != "1212" { - t.Errorf("Wanted 1212, got %s", ms.Calls[0]) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 2) - assertMessages(t, mockContext.SentMessages[0], defaultState.GetKidInfoMessage(models.FullKidInfo{ - Kid: mocks.KidFullInfo.Data, - })) - }) - }) - t.Run("If student absent", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("Cookie") - payload := serdes.Serialize(models.StartPayload{ - Action: models.GetKidInfo, - Payload: []string{"1", "2"}, - }) - mockContext.SetPayload(payload) - mockContext.SetUserMessage(12, "/start="+payload) - ms.FullKidInfoErr = errors.New("") - - messageHandler.Handle(&mockContext) - if ms.Calls[0] != "1212" { - t.Errorf("Wanted 1212, got %s", ms.Calls[0]) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 2) - assertMessages(t, mockContext.SentMessages[0], defaultState.GetKidInfoMessage(models.FullKidInfo{ - Extra: models.NotAccessible, - Kid: mocks.KidFullInfo.Data, - })) - }) - }) - t.Run("Send get /abs", func(t *testing.T) { - t.Run("With payload", func(t *testing.T) { - gr := models.Group{ - GroupID: 1, - Title: "Title", - TimeLesson: getDayByTime(28, 10, 0), - } - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.Actual = models.ActualInformation{ - LessonTitle: "LTitle", - LessonId: 0, - MissingKids: []models.MissingKid{ - { - Id: 1, - Count: 0, - }, { - Id: 2, - Count: 0, - }, - }, - } - ms.AllNames = models.AllKids{ - 1: models.KidData{ - FullName: "vasya", - }, - 2: models.KidData{ - FullName: "petya", - }, - 3: models.KidData{ - FullName: "kirill", - }, - } - ms.SetCurrentGroup(&gr) - - mockContext := mocks.MockContext{} - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - mockContext.SetPayload("2025-02-01 9:32") - mockContext.SetUserMessageWithTime(12, "/abs", getUnixByDay(0, 0, 0)) - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 2) - assertMessages(t, mockContext.SentMessages[0], "Группа по курсу: Title\nЛекция: LTitle\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\nvasya\npetya\n```") - - wantedMarkup := telebot.ReplyMarkup{ResizeKeyboard: true} - wantedMarkup.Inline( - wantedMarkup.Row(wantedMarkup.Data(config.CloseLessonBtn, "close_lesson_1_0"), wantedMarkup.Data(config.OpenLessonBtn, "open_lesson_1_0")), - wantedMarkup.Row(wantedMarkup.Data(config.GetCredsBtn, "get_creds_1")), - ) - assertKeyboards(t, mockContext.SentMessages[0], &wantedMarkup) - if ms.TimeAbs != time.Date(2025, 2, 1, 9, 32, 0, 0, time.UTC) { - t.Errorf("Not mathces dates!") - } - }) - t.Run("Without payload", func(t *testing.T) { - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - ms.SetCurrentGroup(nil) - - mockContext := mocks.MockContext{} - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - mockContext.SetPayload("") - mockContext.SetUserMessageWithTime(12, "/abs", getUnixByDay(0, 0, 0)) - - messageHandler.Handle(&mockContext) - assertContextOptsLen(t, mockContext.SentMessages[0], 0) - assertMessages(t, mockContext.SentMessages[0], "Формат сообщения - '/abs 2025-01-12 15:32'\nВыдаст статистику за 2025г. 12 Января, 15ч 32м") - }) - }) - t.Run("Send AI", func(t *testing.T) { - mockContext := mocks.MockContext{} - - ms := mocks.NewMockService(map[int64]bool{ - 12: true, - }) - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.Default) - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - ms.SetMockCookie("Cookie") - mockContext.SetUserMessage(12, config.AIBtn.Text) - - messageHandler.Handle(&mockContext) - if mockState.Current != stateMachine.ChattingAI { - t.Errorf("wanted %v, got %v", stateMachine.ChattingAI, mockState.Current) - } - assertMessages(t, mockContext.SentMessages[0], "Привет! Используй чат и клавиатуру для общения со мной!") - assertKeyboards(t, mockContext.SentMessages[0], config.AIKeyboard) - }) - }) -} - -func containtsMessages(t *testing.T, excepted mocks.SentMessage, wanted string) { - t.Helper() - - if strings.Contains(excepted.What.(string), wanted) { - - t.Fatalf("Wanted %s, got %s", wanted, excepted.What.(string)) - } -} - -func assertMessages(t *testing.T, got mocks.SentMessage, wantedText string) { - t.Helper() - - if got.What.(string) != wantedText { - t.Errorf("MESSAGES ERROR\n") - t.Errorf("Wanted [%s],\n but got [%s]", wantedText, got.What.(string)) - } -} -func assertKeyboards(t *testing.T, got mocks.SentMessage, wantedMarkup *telebot.ReplyMarkup) { - t.Helper() - - var gotMarkup *telebot.ReplyMarkup - for _, opt := range got.Opts { - if markup, ok := opt.(*telebot.ReplyMarkup); ok { - gotMarkup = markup - break - } - } - - if !reflect.DeepEqual(gotMarkup, wantedMarkup) { - t.Errorf("Wanted keyboard [%+v],\n but got [%+v]", wantedMarkup, gotMarkup) - } -} - -func assertContextOptsLen(t *testing.T, sent mocks.SentMessage, i int) { - t.Helper() - - if len(sent.Opts) != i { - t.Errorf("OPTS LEN ERROR\n") - t.Errorf("%+v\n", sent) - t.Errorf("Wanted context len = %d, got, %d", i, len(sent.Opts)) - } -} - -// getDayByTime 28, 21 вск || 27, 20 сб -func getDayByTime(day, hour, min int) time.Time { - return time.Date(2025, 9, day, hour, min, 0, 0, time.UTC) -} - -// getUnixByDay поскольку телеграм переводит из Unix время в мое время на операционной системе, нужно предварительно при переводе вычесть разницу времен что бы перевод был корректен -func getUnixByDay(day, hour, min int) int64 { - utcTime := time.Date(2025, 9, day, hour, min, 0, 0, time.UTC) - _, offset := time.Now().Zone() - - unixTime := utcTime.Add(-time.Duration(offset) * time.Second).Unix() - return unixTime -} diff --git a/tests/handlers/sendCookieState/sendCookieHandler_test.go b/tests/handlers/sendCookieState/sendCookieHandler_test.go deleted file mode 100644 index da8ec11..0000000 --- a/tests/handlers/sendCookieState/sendCookieHandler_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package test - -import ( - "algobot/internal_old/config" - "algobot/internal_old/contextHandlers" - "algobot/internal_old/stateMachine" - "algobot/tests/mocks" - "github.com/golang/mock/gomock" - "gopkg.in/telebot.v4" - "reflect" - "testing" -) - -func TestSending(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockAI := mocks.NewMockAIService(ctrl) - - t.Run("Send reject action", func(t *testing.T) { - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.SendingCookie) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, "Отменить действие") - - messageHandler.Handle(&mockContext) - - if mockState.Current != stateMachine.Default { - t.Fatalf("Wanted default got %+v\n", mockState.Current) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.StartText) - assertKeyboards(t, mockContext.SentMessages[0], config.StartKeyboard) - }) - t.Run("Send cookie", func(t *testing.T) { - ms := mocks.NewMockService(make(map[int64]bool)) - - mockContext := mocks.MockContext{} - - mockState := mocks.MockStateMachine{} - mockState.SetStatement(12, stateMachine.SendingCookie) - - messageHandler := contextHandlers.NewOnText(ms, &mockState, mockAI) - - mockContext.SetUserMessage(12, "aezakmi") - - messageHandler.Handle(&mockContext) - - if mockState.Current != stateMachine.Default { - t.Fatalf("Wanted default got %+v\n", mockState.Current) - } - if ms.SettedCookie[0] != "12" || ms.SettedCookie[1] != "aezakmi" { - t.Fatalf("Wanted setted cookie, got %+v\n", ms.SettedCookie) - } - assertContextOptsLen(t, mockContext.SentMessages[0], 1) - assertMessages(t, mockContext.SentMessages[0], config.CookieSet) - assertKeyboards(t, mockContext.SentMessages[0], config.StartKeyboard) - }) -} - -func assertMessages(t *testing.T, got mocks.SentMessage, wantedText string) { - t.Helper() - - if got.What.(string) != wantedText { - t.Errorf("Wanted [%s], but got [%s]", wantedText, got.What.(string)) - } -} -func assertKeyboards(t *testing.T, got mocks.SentMessage, wantedMarkup *telebot.ReplyMarkup) { - t.Helper() - - var gotMarkup *telebot.ReplyMarkup - for _, opt := range got.Opts { - if markup, ok := opt.(*telebot.ReplyMarkup); ok { - gotMarkup = markup - break - } - } - - if !reflect.DeepEqual(gotMarkup, wantedMarkup) { - t.Errorf("Wanted keyboard [%+v],\n but got [%+v]", wantedMarkup, gotMarkup) - } -} -func assertContextOptsLen(t *testing.T, sent mocks.SentMessage, i int) { - t.Helper() - - if len(sent.Opts) != i { - t.Errorf("%+v\n", sent) - t.Errorf("Wanted context len = %d, got, %d", i, len(sent.Opts)) - } -} diff --git a/tests/mockgen.go b/tests/mockgen.go deleted file mode 100644 index 293f249..0000000 --- a/tests/mockgen.go +++ /dev/null @@ -1,6 +0,0 @@ -package tests - -//go:generate mockgen -destination=./mocks/AIService_mock.go -package=mocks tgbot/internal/service AIService - -func Dummy() { -} diff --git a/tests/mocks/AIService_mock.go b/tests/mocks/AIService_mock.go deleted file mode 100644 index 81a7b0b..0000000 --- a/tests/mocks/AIService_mock.go +++ /dev/null @@ -1,63 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: tgbot/internal_old/service (interfaces: AIService) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockAIService is a mock of AIService interface. -type MockAIService struct { - ctrl *gomock.Controller - recorder *MockAIServiceMockRecorder -} - -// MockAIServiceMockRecorder is the mock recorder for MockAIService. -type MockAIServiceMockRecorder struct { - mock *MockAIService -} - -// NewMockAIService creates a new mock instance. -func NewMockAIService(ctrl *gomock.Controller) *MockAIService { - mock := &MockAIService{ctrl: ctrl} - mock.recorder = &MockAIServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAIService) EXPECT() *MockAIServiceMockRecorder { - return m.recorder -} - -// ClearAllHistory mocks base method. -func (m *MockAIService) ClearAllHistory(arg0 int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClearAllHistory", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// ClearAllHistory indicates an expected call of ClearAllHistory. -func (mr *MockAIServiceMockRecorder) ClearAllHistory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearAllHistory", reflect.TypeOf((*MockAIService)(nil).ClearAllHistory), arg0) -} - -// GetSuggestion mocks base method. -func (m *MockAIService) GetSuggestion(arg0 int64, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSuggestion", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSuggestion indicates an expected call of GetSuggestion. -func (mr *MockAIServiceMockRecorder) GetSuggestion(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSuggestion", reflect.TypeOf((*MockAIService)(nil).GetSuggestion), arg0, arg1) -} diff --git a/tests/mocks/MockContext.go b/tests/mocks/MockContext.go deleted file mode 100644 index 52483d3..0000000 --- a/tests/mocks/MockContext.go +++ /dev/null @@ -1,73 +0,0 @@ -package mocks - -import "gopkg.in/telebot.v4" - -type SentMessage struct { - What interface{} - Opts []interface{} -} - -type MockContext struct { - userId int64 - userMessage string - unixTime int64 - telebot.Context - - SentMessages []SentMessage - payload string -} - -func (m *MockContext) Sender() *telebot.User { - return &telebot.User{ - ID: m.userId, - } -} - -func (m *MockContext) Send(what interface{}, opts ...interface{}) error { - m.SentMessages = append(m.SentMessages, SentMessage{ - What: what, - Opts: opts, - }) - return nil -} -func (m *MockContext) Edit(what interface{}, opts ...interface{}) error { - m.SentMessages = append(m.SentMessages, SentMessage{ - What: what, - Opts: opts, - }) - return nil -} - -func (m *MockContext) Message() *telebot.Message { - return &telebot.Message{ - Sender: &telebot.User{ - ID: m.userId, - }, - Text: m.userMessage, - Unixtime: m.unixTime, - Payload: m.payload, - } -} - -func (m *MockContext) SetUserMessage(uid int64, msg string) { - m.userId = uid - m.userMessage = msg -} - -func (m *MockContext) SetUserMessageWithTime(uid int64, msg string, unix int64) { - m.userId = uid - m.userMessage = msg - m.unixTime = unix -} -func (m *MockContext) Callback() *telebot.Callback { - return &telebot.Callback{ - Sender: &telebot.User{ - ID: m.userId, - }, - Data: m.userMessage, - } -} - -func (m *MockContext) SetPayload(s string) { - m.payload = s -} diff --git a/tests/mocks/MockStateMachine.go b/tests/mocks/MockStateMachine.go deleted file mode 100644 index 864756d..0000000 --- a/tests/mocks/MockStateMachine.go +++ /dev/null @@ -1,24 +0,0 @@ -package mocks - -import ( - "algobot/internal_old/stateMachine" - "strconv" -) - -type MockStateMachine struct { - Calls []string - Current stateMachine.Statement -} - -func (m *MockStateMachine) GetStatement(uid int64) stateMachine.Statement { - m.Calls = append(m.Calls, "GetStatement") - m.Calls = append(m.Calls, strconv.FormatInt(uid, 10)) - return m.Current -} - -func (m *MockStateMachine) SetStatement(uid int64, statement stateMachine.Statement) { - m.Calls = append(m.Calls, "SetStatement") - m.Calls = append(m.Calls, strconv.FormatInt(uid, 10)) - m.Calls = append(m.Calls, statement.String()) - m.Current = statement -} diff --git a/tests/mocks/mockBot.go b/tests/mocks/mockBot.go deleted file mode 100644 index 47bac3b..0000000 --- a/tests/mocks/mockBot.go +++ /dev/null @@ -1,16 +0,0 @@ -package mocks - -import ( - "fmt" - "gopkg.in/telebot.v4" -) - -type MockBot struct { - telebot.API - Calls []string -} - -func (b *MockBot) Send(to telebot.Recipient, what interface{}, opts ...interface{}) (*telebot.Message, error) { - b.Calls = append(b.Calls, fmt.Sprintf("Send(%s) = %s", to.Recipient(), what)) - return nil, nil -} diff --git a/tests/mocks/mockDomain.go b/tests/mocks/mockDomain.go deleted file mode 100644 index 27ae17e..0000000 --- a/tests/mocks/mockDomain.go +++ /dev/null @@ -1,98 +0,0 @@ -package mocks - -import ( - "algobot/internal_old/domain" - "time" -) - -type MockDomain struct { - MockGroups []domain.Group - errCookie error - errNotif error - DataNotif string -} - -func (m *MockDomain) LastNotificationDate(uid int64) (string, error) { - return "14 дек. `24, 18:36", nil -} - -func (m *MockDomain) SetLastNotificationDate(uid int64, data string) error { - m.DataNotif = data - return nil -} - -func (m *MockDomain) GetUsersByNotification(notifications int) ([]domain.User, error) { - return []domain.User{ - { - UID: 1, - Cookie: "2", - UserAgent: "2", - Notifications: false, - Groups: nil, - }, - }, nil -} - -func (m *MockDomain) User(uid int64) (domain.User, error) { - //TODO implement me - panic("implement me") -} - -func (m *MockDomain) Cookie(uid int64) (string, error) { - if m.errCookie == nil { - return "cookie", nil - } - return "", m.errCookie -} - -func (m *MockDomain) SetCookie(uid int64, cookie string) error { - //TODO implement me - panic("implement me") -} - -func (m *MockDomain) SetUserAgent(uid int64, agent string) error { - //TODO implement me - panic("implement me") -} - -func (m *MockDomain) Groups(uid int64) ([]domain.Group, error) { - return []domain.Group{ - { - GroupID: 1, - Title: "test1", - TimeLesson: time.Date(2024, 10, 6, 14, 55, 55, 3, time.UTC), - }, - { - GroupID: 1, - Title: "test2", - TimeLesson: time.Date(2024, 11, 3, 14, 55, 55, 3, time.UTC), - }, - }, nil -} - -func (m *MockDomain) SetGroups(uid int64, groups []domain.Group) error { - m.MockGroups = groups - return nil -} - -func (m *MockDomain) Notification(uid int64) (bool, error) { - if m.errNotif == nil { - return true, nil - } - return false, m.errNotif -} -func (m *MockDomain) SetNotification(uid int64, value bool) error { - return nil -} -func (m *MockDomain) RegisterUser(uid int64) error { - //TODO implement me - panic("implement me") -} - -func (m *MockDomain) SetErrorCookie(err error) { - m.errCookie = err -} - -func (m *MockDomain) SetErrorNotif(err error) { - m.errNotif = err -} diff --git a/tests/mocks/mockWebClient.go b/tests/mocks/mockWebClient.go deleted file mode 100644 index 8b6aede..0000000 --- a/tests/mocks/mockWebClient.go +++ /dev/null @@ -1,268 +0,0 @@ -package mocks - -import ( - "algobot/internal_old/clients" - "time" -) - -type MockWebClient struct { -} - -func (m MockWebClient) GetKidsNamesByGroup(cookie string, group int) (*clients.GroupResponse, error) { - return &MockGroupResponse, nil -} - -func (m MockWebClient) GetKidInfo(cookie string, kidID string) (*clients.FullKidInfo, error) { - return &KidFullInfo, nil -} - -func (m MockWebClient) GetGroupInfo(cookie string, group string) (*clients.FullGroupInfo, error) { - return &clients.FullGroupInfo{ - Status: "active", - Data: clients.GroupDataFull{ - ID: 1, - Title: "Math Group", - Content: "Some group content", - Type: clients.TypeFull{Value: "online", Label: "Online", Tag: "online-tag"}, - Status: clients.StatusFull{Value: 1, Label: "Active", Tag: "active-tag"}, - StatusChangedAt: "2025-02-05T12:00:00Z", - StartTime: "2025-02-06T09:00:00Z", - NextLessonTime: "2025-02-06T10:00:00Z", - LessonsTotal: 20, - LessonsPassed: 5, - HardwareNeeded: 2, - ClientManager: nil, - }, - }, nil -} - -func (m MockWebClient) GetKidsStatsByGroup(cookie, group string) (*clients.KidsStats, error) { - return &kidsStats, nil -} - -func (m MockWebClient) OpenLession(cookie, group, lession string) error { - //TODO implement me - panic("implement me") -} - -func (m MockWebClient) CloseLession(cookie, group, lession string) error { - //TODO implement me - panic("implement me") -} - -func (m MockWebClient) GetKidsMessages(cookie string) (*clients.KidsMessages, error) { - return &clients.KidsMessages{ - Status: "ok", - Data: clients.MessagesData{ - Projects: []clients.Message{ - { - UID: "1", - New: false, - SenderID: 0, - SenderScope: "user", - Type: "", - Content: "1", - Name: "1", - LastTime: "18 янв. 15:09", - Title: "1", - Link: "1", - }, - { - UID: "2", - New: false, - SenderID: 0, - SenderScope: "student", - Type: "", - Content: "2", - Name: "2", - LastTime: "29 дек. `24, 18:51", - Title: "2", - Link: "2", - }, - }, - }, - }, nil -} - -func (m MockWebClient) GetAllGroupsByUser(cookie string) ([]clients.AllGroupsUser, error) { - return []clients.AllGroupsUser{ - { - Title: "Title", - GroupId: "1", - TimeLesson: "01.02.2025 14:00", - RegularTime: "4", - }, - }, nil -} - -var MockGroupResponse = clients.GroupResponse{ - Status: "success", - Data: clients.GroupData{ - Items: []clients.Student{ - { - ID: 1, - FirstName: "Иван", - LastName: "Иванов", - FullName: "Иван Иванов", - ParentName: "Алексей Иванов", - Email: "ivanov@example.com", - HasLaptop: 1, - Phone: "+79161234567", - Age: 16, - BirthDate: time.Date(2008, time.Month(5), 10, 0, 0, 0, 0, time.UTC), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeletedAt: nil, - HasBranchAccess: true, - Username: "ivan_ivanov", - Password: "secret_password_123", - LastGroup: clients.Group{ - ID: 1, - GroupStudentID: 1, - Title: "Группа 1", - Content: "Основы программирования", - Track: 1, - Status: 0, - StartTime: time.Date(2025, time.Month(1), 15, 10, 0, 0, 0, time.UTC), - EndTime: time.Date(2025, time.Month(5), 15, 18, 0, 0, 0, time.UTC), - CourseID: 201, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeletedAt: nil, - }, - Links: clients.Links{ - Self: clients.SelfLink{ - Href: "http://example.com/students/1", - }, - }, - }, - { - ID: 2, - FirstName: "Мария", - LastName: "Петрова", - FullName: "Мария Петрова", - ParentName: "Елена Петрова", - Email: "petrova@example.com", - HasLaptop: 0, - Phone: "+79261234567", - Age: 15, - BirthDate: time.Date(2009, time.Month(7), 20, 0, 0, 0, 0, time.UTC), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeletedAt: nil, - HasBranchAccess: false, - Username: "maria_petrov", - Password: "password_321", - LastGroup: clients.Group{ - ID: 1, - GroupStudentID: 2, - Title: "Группа 2", - Content: "Алгоритмы и структуры данных", - Track: 2, - Status: 0, - StartTime: time.Date(2025, time.Month(2), 1, 10, 0, 0, 0, time.UTC), - EndTime: time.Date(2025, time.Month(6), 1, 18, 0, 0, 0, time.UTC), - CourseID: 202, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeletedAt: nil, - }, - Links: clients.Links{ - Self: clients.SelfLink{ - Href: "http://example.com/students/2", - }, - }, - }, - }, - }, -} - -var KidFullInfo = clients.FullKidInfo{ - Status: "success", - Data: clients.Student{ - ID: 123456, - FirstName: "Иван", - LastName: "Иванов", - FullName: "Иван Иванов", - ParentName: "Мария Иванова", - Email: "ivanov-maria@example.com", - HasLaptop: 1, - Phone: "+7 (800) 123-45-67", - Age: 22, - BirthDate: time.Date(1995, time.July, 15, 0, 0, 0, 0, time.UTC), - CreatedAt: time.Date(2022, time.May, 1, 12, 30, 0, 0, time.UTC), - UpdatedAt: time.Date(2024, time.January, 20, 18, 45, 30, 0, time.UTC), - DeletedAt: nil, - HasBranchAccess: false, - Username: "ivanov123", - Password: "password123", - Groups: []clients.Group{ - { - ID: 987654, - GroupStudentID: 123456, - Title: "Математика 101", - Content: "Основы математики", - Track: 2, - Status: 0, - StartTime: time.Date(2023, time.June, 1, 10, 0, 0, 0, time.UTC), - EndTime: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.UTC), - CourseID: 101, - CreatedAt: time.Date(2023, time.April, 10, 15, 15, 30, 0, time.UTC), - UpdatedAt: time.Date(2024, time.January, 15, 14, 0, 10, 0, time.UTC), - DeletedAt: nil, - }, - }, - }, -} - -var kidsStats = clients.KidsStats{ - Status: "success", - Data: []clients.KidStat{ - { - StudentID: 1, - Attendance: []clients.Attendance{ - { - LessonID: 1, - LessonTitle: "less1", - StartTimeFormatted: "вс 22.09.24 14:00", - Status: "present", - }, - { - LessonID: 2, - LessonTitle: "less2", - StartTimeFormatted: "вс 29.09.24 14:00", - Status: "present", - }, - { - LessonID: 3, - LessonTitle: "less3", - StartTimeFormatted: "вс 06.10.24 14:00", - Status: "present", - }, - }, - }, - { - StudentID: 2, - Attendance: []clients.Attendance{ - { - LessonID: 1, - LessonTitle: "less1", - StartTimeFormatted: "вс 22.09.24 14:00", - Status: "present", - }, - { - LessonID: 2, - LessonTitle: "less2", - StartTimeFormatted: "вс 29.09.24 14:00", - Status: "absent", - }, - { - LessonID: 3, - LessonTitle: "less3", - StartTimeFormatted: "вс 06.10.24 14:00", - Status: "absent", - }, - }, - }, - }, -} diff --git a/tests/mocks/newMockService.go b/tests/mocks/newMockService.go deleted file mode 100644 index edf5528..0000000 --- a/tests/mocks/newMockService.go +++ /dev/null @@ -1,175 +0,0 @@ -package mocks - -import ( - "algobot/internal_old/models" - "errors" - "fmt" - "strconv" - "time" -) - -type MockService struct { - m map[int64]bool - cookie string - StubNotification bool - gr *models.Group - grs []models.Group - SettedCookie []string - Actual models.ActualInformation - AllNames models.AllKids - grErr error - Calls []string - TimeAbs time.Time - FullKidInfoErr error -} - -func (n *MockService) FullGroupInfo(uid int64, groupId int) (models.FullGroupInfo, error) { - n.Calls = append(n.Calls, fmt.Sprintf("%d%d", uid, groupId)) - - return FullGrInfo, nil -} - -func (n *MockService) AllCredentials(uid int64, groupId int) (map[string]string, error) { - n.Calls = append(n.Calls, fmt.Sprintf("AllCredentials(%d, %d)", uid, groupId)) - return map[string]string{ - "Ваня": "van:12", - }, nil -} - -func (n *MockService) ActualInformation(uid int64, t time.Time, groupId int) (models.ActualInformation, error) { - return n.Actual, nil -} - -func (n *MockService) AllKidsNames(uid int64, groupId int) (models.AllKids, error) { - return n.AllNames, nil -} - -func NewMockService(m map[int64]bool) *MockService { - return &MockService{m: m} -} - -func (n *MockService) FullKidInfo(uid int64, kidID int, groupId int) (models.FullKidInfo, error) { - n.Calls = append(n.Calls, fmt.Sprintf("%d%d%d", uid, kidID, groupId)) - if n.FullKidInfoErr != nil { - return models.FullKidInfo{ - Extra: models.NotAccessible, - Kid: KidFullInfo.Data, - }, nil - } - return models.FullKidInfo{ - Kid: KidFullInfo.Data, - }, nil -} - -func (n *MockService) UsersByNotif(status bool) ([]models.ScheduleData, error) { - return []models.ScheduleData{ - { - UID: 1, - Cookie: "c", - }, - }, nil - -} - -func (n *MockService) NewMessageByUID(uid int64) ([]models.Message, error) { - return []models.Message{ - { - Id: "0", - From: "1", - Theme: "2", - Link: "3", - Content: "4", - }, - }, nil -} - -func (n *MockService) CloseLesson(uid int64, lessonId int, groupId int) error { - n.Calls = append(n.Calls, fmt.Sprintf("CloseLesson(%d, %d, %d)", uid, lessonId, groupId)) - return nil -} - -func (n *MockService) OpenLesson(uid int64, lessonId int, groupId int) error { - n.Calls = append(n.Calls, fmt.Sprintf("OpenLesson(%d, %d, %d)", uid, lessonId, groupId)) - return nil -} - -func (n *MockService) SetMockCookie(s string) { - n.cookie = s -} -func (n *MockService) RefreshGroups(uid int64) error { - return nil -} -func (n *MockService) SetCurrentGroup(group *models.Group) { - n.gr = group -} -func (n *MockService) SetGroups(groups []models.Group) { - n.grs = groups -} - -func (n *MockService) CurrentGroup(uid int64, t time.Time) (models.Group, error) { - n.TimeAbs = t - if n.gr == nil { - return models.Group{}, errors.New("no gr") - } - return *n.gr, nil -} - -func (n *MockService) Groups(uid int64) ([]models.Group, error) { - if n.grErr != nil { - return nil, n.grErr - } - if n.grs == nil { - return nil, errors.New("no gr") - } - return n.grs, nil -} - -func (n *MockService) MissingKids(uid int64, t time.Time, g int) ([]string, error) { - //TODO implement me - panic("implement me") -} - -func (n *MockService) Cookie(uid int64) (string, error) { - return n.cookie, nil -} - -func (n *MockService) SetCookie(uid int64, cookie string) error { - n.SettedCookie = []string{strconv.FormatInt(uid, 10), cookie} - return nil -} -func (n *MockService) SetNotification(uid int64, notification bool) error { - n.StubNotification = notification - return nil -} - -func (n *MockService) Notification(uid int64) (bool, error) { - return n.StubNotification, nil -} - -func (n *MockService) IsUserRegistered(uid int64) (bool, error) { - v, ok := n.m[uid] - if ok != true { - return false, nil - } - return v, nil -} - -func (n *MockService) RegisterUser(uid int64) error { - n.m[uid] = true - return nil -} - -func (n *MockService) SetGroupsErr(err error) { - n.grErr = err -} - -var FullGrInfo = models.FullGroupInfo{ - GroupID: 1, - GroupTitle: "Title", - GroupContent: "Content", - NextLessonTime: "15.03.2025 16:00", - LessonsTotal: 20, - LessonsPassed: 10, - ActiveKids: MockGroupResponse.Data.Items, - NotActiveKids: MockGroupResponse.Data.Items, -} diff --git a/tests/scheduler/message_test.go b/tests/scheduler/message_test.go deleted file mode 100644 index da7d837..0000000 --- a/tests/scheduler/message_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package scheduler_test - -import ( - "algobot/internal_old/schedulers" - "algobot/tests/mocks" - "testing" -) - -func TestScheduler(t *testing.T) { - mockService := mocks.NewMockService(nil) - mockBot := mocks.MockBot{} - - scheduler := schedulers.NewMessage(&mockBot, mockService) - scheduler.Schedule() - - if len(mockBot.Calls) != 1 { - t.Errorf("Wanted 1, got %d", len(mockBot.Calls)) - } - wanted := "Send(1) = 🔔 Новое сообщение\n\nОт: 1\nТема: 2\nСсылка: 3\n\n```Сообщение:\n4\n```" - if mockBot.Calls[0] != wanted { - t.Errorf("Wanted %s, got %s", wanted, mockBot.Calls[0]) - } -} diff --git a/tests/services/service_test.go b/tests/services/service_test.go deleted file mode 100644 index 8df2a4e..0000000 --- a/tests/services/service_test.go +++ /dev/null @@ -1,267 +0,0 @@ -package services - -import ( - "algobot/internal_old/domain" - appError "algobot/internal_old/error" - "algobot/internal_old/models" - "algobot/internal_old/service" - "algobot/tests/mocks" - "reflect" - "testing" - "time" -) - -func TestDefaultService(t *testing.T) { - t.Run("Get cookie, with error", func(t *testing.T) { - t.Run("Without error", func(t *testing.T) { - d := mocks.MockDomain{} - defaultService := service.NewDefaultService(&d, mocks.MockWebClient{}) - d.SetErrorCookie(nil) - - c, e := defaultService.Cookie(1) - if e != nil { - t.Fatalf("Wanted no error, got %v", e) - } - if c != "cookie" { - t.Fatalf("Wanted 'cookie', got '%s'", c) - } - }) - t.Run("With error", func(t *testing.T) { - d := mocks.MockDomain{} - defaultService := service.NewDefaultService(&d, mocks.MockWebClient{}) - d.SetErrorCookie(appError.ErrNotValid) - - c, e := defaultService.Cookie(1) - if e != nil { - t.Fatalf("Wanted no error, got %v", e) - } - if c != "" { - t.Fatalf("Wanted '', got %s", c) - } - }) - }) - t.Run("Get notification, with error", func(t *testing.T) { - t.Run("Without error", func(t *testing.T) { - d := mocks.MockDomain{} - defaultService := service.NewDefaultService(&d, mocks.MockWebClient{}) - d.SetErrorNotif(nil) - - c, e := defaultService.Notification(1) - if e != nil { - t.Fatalf("Wanted no error, got %v", e) - } - if c != true { - t.Fatalf("Wanted 'true', got '%v'", c) - } - }) - t.Run("With error", func(t *testing.T) { - d := mocks.MockDomain{} - defaultService := service.NewDefaultService(&d, mocks.MockWebClient{}) - d.SetErrorNotif(appError.ErrNotValid) - - c, e := defaultService.Notification(1) - if e != nil { - t.Fatalf("Wanted no error, got %v", e) - } - if c != false { - t.Fatalf("Wanted 'false', got %v", c) - } - }) - }) - t.Run("Get CurrentGroup", func(t *testing.T) { - defaultService := service.NewDefaultService(&mocks.MockDomain{}, mocks.MockWebClient{}) - group, err := defaultService.CurrentGroup( - 1, - time.Date(2024, 10, 6, 14, 55, 55, 3, time.UTC), - ) - if err != nil { - t.Fatalf("Got error: %v", err) - } - wanted := models.Group{ - GroupID: 1, - Title: "test1", - TimeLesson: time.Date(2024, 10, 6, 14, 55, 55, 3, time.UTC), - } - if !reflect.DeepEqual(wanted, group) { - t.Fatalf("Wanted %v, got %v", wanted, group) - } - }) - t.Run("Get ActualInformation", func(t *testing.T) { - domain := &mocks.MockDomain{} - client := mocks.MockWebClient{} - - defaultService := service.NewDefaultService(domain, client) - group, err := defaultService.ActualInformation( - 1, - time.Date(2024, 10, 6, 14, 55, 55, 3, time.UTC), - 1, - ) - if err != nil { - t.Fatalf("Got error: %v", err) - } - wanted := models.ActualInformation{ - LessonTitle: "less3", - LessonId: 3, - MissingKids: []models.MissingKid{ - { - Id: 2, - Count: 2, - }, - }, - } - if !reflect.DeepEqual(wanted, group) { - t.Fatalf("Wanted %v, got %v", wanted, group) - } - }) - t.Run("Get AllKidsNames", func(t *testing.T) { - domain := &mocks.MockDomain{} - client := mocks.MockWebClient{} - - defaultService := service.NewDefaultService(domain, client) - group, err := defaultService.AllKidsNames( - 1, - 1, - ) - if err != nil { - t.Fatalf("Got error: %v", err) - } - wanted := models.AllKids{ - 1: models.KidData{ - FullName: "Иван Иванов", - Login: "ivan_ivanov", - Password: "secret_password_123", - }, - 2: models.KidData{ - FullName: "Мария Петрова", - Login: "maria_petrov", - Password: "password_321", - }, - } - if !reflect.DeepEqual(wanted, group) { - t.Fatalf("Wanted %#v, got %#v", wanted, group) - } - }) - t.Run("Refresh groups", func(t *testing.T) { - d := mocks.MockDomain{} - defaultService := service.NewDefaultService(&d, mocks.MockWebClient{}) - err := defaultService.RefreshGroups(1) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - wanted := domain.Group{ - GroupID: 1, - Title: "Title", - TimeLesson: time.Date(2025, 2, 1, 14, 00, 00, 00, time.UTC), - } - - if !reflect.DeepEqual(d.MockGroups[0], wanted) { - t.Fatalf("Wanted %#v, got %#v", wanted, d.MockGroups[0]) - } - }) - t.Run("AllCredentials", func(t *testing.T) { - d := mocks.MockDomain{} - webClient := mocks.MockWebClient{} - defaultService := service.NewDefaultService(&d, webClient) - - creds, err := defaultService.AllCredentials(1, 1) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - wanted := map[string]string{ - "Иван Иванов": "ivan_ivanov:secret_password_123", - "Мария Петрова": "maria_petrov:password_321", - } - - if !reflect.DeepEqual(creds, wanted) { - t.Fatalf("Wanted %#v, got %#v", wanted, creds) - } - }) - t.Run("UserUidsByNotif", func(t *testing.T) { - d := mocks.MockDomain{} - webClient := mocks.MockWebClient{} - defaultService := service.NewDefaultService(&d, webClient) - - uids, err := defaultService.UsersByNotif(true) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - wanted := []models.ScheduleData{ - { - UID: 1, - Cookie: "2", - }, - } - if !reflect.DeepEqual(uids, wanted) { - t.Fatalf("Wanted %#v, got %#v", wanted, uids) - } - }) - t.Run("NewMessageByUID", func(t *testing.T) { - d := mocks.MockDomain{} - webClient := mocks.MockWebClient{} - defaultService := service.NewDefaultService(&d, webClient) - - msgs, err := defaultService.NewMessageByUID(1) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - wanted := []models.Message{ - { - Id: "2", - From: "2", - Theme: "2", - Link: "https://backoffice.algoritmika.org2", - Content: "2", - }, - } - if !reflect.DeepEqual(msgs, wanted) { - t.Fatalf("Wanted %#v, got %#v", wanted, msgs) - } - - wantedDate := "29 дек. `24, 18:51" - if d.DataNotif != wantedDate { - t.Fatalf("Wanted %s, but got %s", wantedDate, d.DataNotif) - } - }) - t.Run("FullGroupInfo", func(t *testing.T) { - d := mocks.MockDomain{} - webClient := mocks.MockWebClient{} - defaultService := service.NewDefaultService(&d, webClient) - - got, err := defaultService.FullGroupInfo(1, 1) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - wanted := models.FullGroupInfo{ - GroupID: 1, - GroupTitle: "Math Group", - GroupContent: "Some group content", - NextLessonTime: "2025-02-06T10:00:00Z", - LessonsPassed: 5, - LessonsTotal: 20, - ActiveKids: mocks.MockGroupResponse.Data.Items, - NotActiveKids: nil, - } - if !reflect.DeepEqual(got, wanted) { - t.Fatalf("Wanted %#v, got %#v", wanted, got) - } - }) - t.Run("FullKidInfo", func(t *testing.T) { - d := mocks.MockDomain{} - webClient := mocks.MockWebClient{} - defaultService := service.NewDefaultService(&d, webClient) - - got, err := defaultService.FullKidInfo(1, 1, 1) - if err != nil { - t.Fatalf("Got error: %v", err) - } - - if !reflect.DeepEqual(got.Kid, mocks.KidFullInfo.Data) { - t.Fatalf("Wanted %#v, got %#v", mocks.KidFullInfo, got) - } - }) -} diff --git a/tests/stateMachine_test.go b/tests/stateMachine_test.go deleted file mode 100644 index e40bb21..0000000 --- a/tests/stateMachine_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package tests_test - -import ( - stateMachine2 "algobot/internal_old/stateMachine" - "testing" -) - -func TestState(t *testing.T) { - t.Run("Set state", func(t *testing.T) { - sm := stateMachine2.NewMemory() - - sm.SetStatement(1, stateMachine2.Default) - got := sm.GetStatement(1) - if got != stateMachine2.Default { - t.Fatalf("Wanted state %s, but got %s", stateMachine2.Default, got) - } - }) - t.Run("Get state if not exists", func(t *testing.T) { - sm := stateMachine2.NewMemory() - - got := sm.GetStatement(1) - if got != stateMachine2.Default { - t.Fatalf("Wanted state %s, but got %s", stateMachine2.Default, got) - } - }) -} From 1f40d373828815d1d38f2a5e6888ad57954a63fe Mon Sep 17 00:00:00 2001 From: danil227pavlov Date: Tue, 1 Apr 2025 23:37:07 +0300 Subject: [PATCH 06/44] add /start, settings, set_cookie handler + tests --- internal/app/app.go | 4 +- internal/app/telegram/app.go | 27 +++++++- .../telegram/keyboards/sendingCookie.go | 14 ++++ .../domain/telegram/keyboards/settings.go | 16 +++++ internal/domain/telegram/keyboards/start.go | 20 +++--- .../handlers/callback/changeCookie.go | 24 +++++++ internal/telegram/handlers/text/settings.go | 59 +++++++++++++++++ internal/telegram/middleware/logger/logger.go | 3 + internal/telegram/middleware/trace/trace.go | 4 +- test/mocks/telegram/handlers/mockgen.go | 2 + test/telegram/handlers/changecookie_test.go | 30 +++++++++ test/telegram/handlers/settings_test.go | 64 +++++++++++++++++++ 12 files changed, 249 insertions(+), 18 deletions(-) create mode 100644 internal/domain/telegram/keyboards/sendingCookie.go create mode 100644 internal/domain/telegram/keyboards/settings.go create mode 100644 internal/telegram/handlers/callback/changeCookie.go create mode 100644 internal/telegram/handlers/text/settings.go create mode 100644 test/telegram/handlers/changecookie_test.go create mode 100644 test/telegram/handlers/settings_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 92b168a..d85261b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -14,7 +14,7 @@ type App struct { func New(log *slog.Logger, cfg *config.Config) *App { - botApplicaton := telegram.New(log, cfg.TelegramToken) + botApplication := telegram.New(log, cfg.TelegramToken) - return &App{log: log, cfg: cfg, TelegramBot: botApplicaton} + return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 9a1521c..0a96d8a 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -4,10 +4,13 @@ import ( "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" + "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/logger" "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" + "errors" + "fmt" router "github.com/LZTD1/telebot-router" tele "gopkg.in/telebot.v4" "gopkg.in/telebot.v4/middleware" @@ -33,6 +36,11 @@ func New(log *slog.Logger, token string) *App { Poller: &tele.LongPoller{ Timeout: 10 * time.Second, }, + OnError: func(e error, c tele.Context) { // TODO : refactor into handler + traceID := c.Get("trace_id") // TODO : maybe send warnings to admin ? + c.Send(fmt.Sprintf("[%s]\n\nУпс! Произошла какая-то непредвиденная ошибка!\nОбратитесь к администратору", traceID), tele.ModeHTML) + log.Warn("can`t handle error", sl.Err(e), slog.Any("trace_id", traceID)) + }, } b, err := tele.NewBot(pref) if err != nil { @@ -47,16 +55,29 @@ func New(log *slog.Logger, token string) *App { b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) - r := router.NewRouter() - r.Group(func(r router.Router) { + r.Group(func(r router.Router) { // Routes for default state r.Use(stater.New(stateMachine, fsm.Default)) + // message r.HandleFuncText("/start", text.NewStart(stateMachine)) + r.HandleFuncText("Настройки", text.NewSettings(nil, nil)) + + // callbacks + r.HandleFuncCallback("set_cookie", callback.NewChangeCookie(nil)) + r.HandleFuncCallback("change_notification", nil) }) - b.Handle(tele.OnText, r.ServeContext) + r.Group(func(r router.Router) { // Routes for default state + r.Use(stater.New(stateMachine, fsm.SendingCookie)) + + r.HandleFuncText("Отменить действие", text.NewStart(stateMachine)) + }) + + b.Handle(tele.OnText, func(context tele.Context) error { + return errors.New("error") + }) return &App{log: log, bot: b} } diff --git a/internal/domain/telegram/keyboards/sendingCookie.go b/internal/domain/telegram/keyboards/sendingCookie.go new file mode 100644 index 0000000..1b5f474 --- /dev/null +++ b/internal/domain/telegram/keyboards/sendingCookie.go @@ -0,0 +1,14 @@ +package keyboards + +import tele "gopkg.in/telebot.v4" + +func RejectKeyboard() *tele.ReplyMarkup { + rejectKb := &tele.ReplyMarkup{ResizeKeyboard: true} + reject := rejectKb.Text("Отменить действие") + + rejectKb.Reply( + rejectKb.Row(reject), + ) + + return rejectKb +} diff --git a/internal/domain/telegram/keyboards/settings.go b/internal/domain/telegram/keyboards/settings.go new file mode 100644 index 0000000..64cfa81 --- /dev/null +++ b/internal/domain/telegram/keyboards/settings.go @@ -0,0 +1,16 @@ +package keyboards + +import tele "gopkg.in/telebot.v4" + +func Settings() *tele.ReplyMarkup { + settingsKb := &tele.ReplyMarkup{ResizeKeyboard: true} + setCookie := settingsKb.Data("Установить Cookie", "set_cookie") + changeNotification := settingsKb.Data("Переключить уведомления", "change_notification") + + settingsKb.Inline( + settingsKb.Row(setCookie), + settingsKb.Row(changeNotification), + ) + + return settingsKb +} diff --git a/internal/domain/telegram/keyboards/start.go b/internal/domain/telegram/keyboards/start.go index 6f4abd4..c26679d 100644 --- a/internal/domain/telegram/keyboards/start.go +++ b/internal/domain/telegram/keyboards/start.go @@ -3,18 +3,18 @@ package keyboards import tele "gopkg.in/telebot.v4" func Start() *tele.ReplyMarkup { - StartKeyboard := &tele.ReplyMarkup{ResizeKeyboard: true} + startKb := &tele.ReplyMarkup{ResizeKeyboard: true} - MissingBtn := StartKeyboard.Text("Получить отсутсвующих") - MyGroupsBtn := StartKeyboard.Text("Мои группы") - SettingsBtn := StartKeyboard.Text("Настройки") - AIBtn := StartKeyboard.Text("AI 🔹") + missing := startKb.Text("Получить отсутсвующих") + myGroups := startKb.Text("Мои группы") + settings := startKb.Text("Настройки") + ai := startKb.Text("AI 🔹") - StartKeyboard.Reply( - StartKeyboard.Row(MissingBtn), - StartKeyboard.Row(MyGroupsBtn, SettingsBtn), - StartKeyboard.Row(AIBtn), + startKb.Reply( + startKb.Row(missing), + startKb.Row(myGroups, settings), + startKb.Row(ai), ) - return StartKeyboard + return startKb } diff --git a/internal/telegram/handlers/callback/changeCookie.go b/internal/telegram/handlers/callback/changeCookie.go new file mode 100644 index 0000000..6afe7dd --- /dev/null +++ b/internal/telegram/handlers/callback/changeCookie.go @@ -0,0 +1,24 @@ +package callback + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "gopkg.in/telebot.v4" +) + +type StateChanger interface { + SetState(uid int64, state fsm.State) +} + +func NewChangeCookie(stateChanger StateChanger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + uid := ctx.Sender().ID + + stateChanger.SetState(uid, fsm.SendingCookie) + + return ctx.Send( + "Отправьте мне свои cookie 🍪\nИнструкция: https://telegra.ph/Kak-dobavit-v-bota-svoi-Cookie-02-05", + keyboards.RejectKeyboard(), + ) + } +} diff --git a/internal/telegram/handlers/text/settings.go b/internal/telegram/handlers/text/settings.go new file mode 100644 index 0000000..8c44b44 --- /dev/null +++ b/internal/telegram/handlers/text/settings.go @@ -0,0 +1,59 @@ +package text + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type UserInformer interface { + Cookies(uid int64) (string, error) + Notification(uid int64) (bool, error) +} + +func NewSettings(uInformer UserInformer, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewSettings" + + uid := ctx.Sender().ID + log = log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + + cookies, err := uInformer.Cookies(uid) + if err != nil { + log.Warn("error while get cookies", sl.Err(err)) + return fmt.Errorf("%s: error while get cookies %w", op, err) + } + + notification, err := uInformer.Notification(uid) + if err != nil { + log.Warn("error while get notification", sl.Err(err)) + return fmt.Errorf("%s: error while get notification %w", op, err) + } + + return ctx.Send(GetSettingsMessage(cookies, notification), keyboards.Settings()) + } +} + +func GetSettingsMessage(cookies string, notification bool) string { + sb := strings.Builder{} + sb.WriteString("🔧 Ваши настройки:\n") + sb.WriteString("\nКуки: ") + if cookies != "" { + sb.WriteString("✅") + } else { + sb.WriteString("✖️") + } + sb.WriteString("\nУведомление от чата:") + if !notification { + sb.WriteString("✅") + } else { + sb.WriteString("✖️") + } + return sb.String() +} diff --git a/internal/telegram/middleware/logger/logger.go b/internal/telegram/middleware/logger/logger.go index 5229302..dcb423e 100644 --- a/internal/telegram/middleware/logger/logger.go +++ b/internal/telegram/middleware/logger/logger.go @@ -13,12 +13,15 @@ func New(log *slog.Logger) tele.MiddlewareFunc { ) return func(c tele.Context) error { + traceID := c.Get("trace_id") + if msg := c.Message(); msg != nil { log.Info("incoming message", slog.Int64("from_id", c.Sender().ID), slog.String("from", c.Sender().Username), slog.String("full_name", fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName)), slog.String("message", msg.Text), + slog.Any("trace_id", traceID), ) } if cb := c.Callback(); cb != nil { diff --git a/internal/telegram/middleware/trace/trace.go b/internal/telegram/middleware/trace/trace.go index 962060f..e9cd04e 100644 --- a/internal/telegram/middleware/trace/trace.go +++ b/internal/telegram/middleware/trace/trace.go @@ -1,7 +1,6 @@ package trace import ( - "fmt" "github.com/google/uuid" tele "gopkg.in/telebot.v4" "log/slog" @@ -22,9 +21,8 @@ func New(log *slog.Logger) tele.MiddlewareFunc { if err != nil { log.Warn("failed to generate UUID") } - traceID := fmt.Sprintf("%d/%s/%s", c.Sender().ID, c.Sender().Username, newUUID.String()) - c.Set("trace_id", traceID) + c.Set("trace_id", newUUID.String()) return next(c) } diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 44dab10..3fd7d31 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -1,3 +1,5 @@ package mocks //go:generate mockgen -destination=./set_stater_mock.go -package=mocks algobot/internal/telegram/handlers/text SetStater +//go:generate mockgen -destination=./userInformer_mock.go -package=mocks algobot/internal/telegram/handlers/text UserInformer +//go:generate mockgen -destination=./stateChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback StateChanger diff --git a/test/telegram/handlers/changecookie_test.go b/test/telegram/handlers/changecookie_test.go new file mode 100644 index 0000000..492f4a8 --- /dev/null +++ b/test/telegram/handlers/changecookie_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/telegram/handlers/callback" + mocks2 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" +) + +func TestChangeCookie(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stater := mocks.NewMockStateChanger(ctrl) + mctx := mocks2.NewMockContext(ctrl) + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&telebot.User{ID: 1}).Times(1), + stater.EXPECT().SetState(int64(1), fsm.SendingCookie).Times(1), + mctx.EXPECT().Send("Отправьте мне свои cookie 🍪\nИнструкция: https://telegra.ph/Kak-dobavit-v-bota-svoi-Cookie-02-05", keyboards.RejectKeyboard()).Return(nil).Times(1), + ) + + err := callback.NewChangeCookie(stater)(mctx) + assert.NoError(t, err) +} diff --git a/test/telegram/handlers/settings_test.go b/test/telegram/handlers/settings_test.go new file mode 100644 index 0000000..e1953d3 --- /dev/null +++ b/test/telegram/handlers/settings_test.go @@ -0,0 +1,64 @@ +package test + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/telegram/handlers/text" + mocks3 "algobot/test/mocks" + mocks2 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestSettings(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + uinformer := mocks.NewMockUserInformer(ctrl) + mctx := mocks2.NewMockContext(ctrl) + log := mocks3.NewMockLogger() + + t.Run("Happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Get(gomock.Any()).Return("a-1").Times(1), + uinformer.EXPECT().Cookies(int64(1)).Return("", nil).Times(1), + uinformer.EXPECT().Notification(int64(1)).Return(true, nil).Times(1), + mctx.EXPECT().Send(text.GetSettingsMessage("", true), keyboards.Settings()).Return(nil).Times(1), + ) + + settings := text.NewSettings(uinformer, log) + err := settings(mctx) + assert.NoError(t, err) + }) + t.Run("Cookie returns err", func(t *testing.T) { + errCookie := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Get(gomock.Any()).Return("a-1").Times(1), + uinformer.EXPECT().Cookies(int64(1)).Return("", errCookie).Times(1), + ) + + settings := text.NewSettings(uinformer, log) + err := settings(mctx) + assert.ErrorIs(t, err, errCookie) + }) + t.Run("Notification returns err", func(t *testing.T) { + errNotification := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Get(gomock.Any()).Return("a-1").Times(1), + uinformer.EXPECT().Cookies(int64(1)).Return("", nil).Times(1), + uinformer.EXPECT().Notification(int64(1)).Return(false, errNotification).Times(1), + ) + + settings := text.NewSettings(uinformer, log) + err := settings(mctx) + assert.ErrorIs(t, err, errNotification) + }) +} From 031335c22bc178bc5951d82f993c27de581d45fc Mon Sep 17 00:00:00 2001 From: pavlov Date: Wed, 2 Apr 2025 10:54:53 +0300 Subject: [PATCH 07/44] add changeNotification handler --- Makefile | 7 ++- go.mod | 2 +- go.sum | 2 + internal/app/telegram/app.go | 3 +- .../handlers/callback/changeNotification.go | 38 +++++++++++++ test/mocks/telegram/handlers/mockgen.go | 1 + .../handlers/changeNotification_test.go | 57 +++++++++++++++++++ 7 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 internal/telegram/handlers/callback/changeNotification.go create mode 100644 test/telegram/handlers/changeNotification_test.go diff --git a/Makefile b/Makefile index 71d6054..46e6797 100644 --- a/Makefile +++ b/Makefile @@ -8,4 +8,9 @@ gen: .PHONY: dev dev: - go run ./cmd/algobot/main.go -config=./config/local.yaml \ No newline at end of file + go run ./cmd/algobot/main.go -config=./config/local.yaml + + +.PHONY: mock-gen +mock-gen: + cd test && go generate ./... \ No newline at end of file diff --git a/go.mod b/go.mod index 2024750..b7cd0f3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module algobot go 1.23.3 require ( - github.com/LZTD1/telebot-router v1.0.0 + github.com/LZTD1/telebot-router v1.1.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index c56742f..2a4f7b7 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/LZTD1/telebot-router v1.0.0 h1:Fskz8zTCfFKrrOg4LTiBgQYZXKj4nxjfVmlZRmfZzGQ= github.com/LZTD1/telebot-router v1.0.0/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= +github.com/LZTD1/telebot-router v1.1.1 h1:YNIs70tnMSjpZNFQxYhS6GcUfPBugR5FSUlJ1S3mvrc= +github.com/LZTD1/telebot-router v1.1.1/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 0a96d8a..6d59e58 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -55,6 +55,7 @@ func New(log *slog.Logger, token string) *App { b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) + r := router.NewRouter() r.Group(func(r router.Router) { // Routes for default state @@ -69,7 +70,7 @@ func New(log *slog.Logger, token string) *App { r.HandleFuncCallback("change_notification", nil) }) - r.Group(func(r router.Router) { // Routes for default state + r.Group(func(r router.Router) { // Routes for SendingCookie state r.Use(stater.New(stateMachine, fsm.SendingCookie)) r.HandleFuncText("Отменить действие", text.NewStart(stateMachine)) diff --git a/internal/telegram/handlers/callback/changeNotification.go b/internal/telegram/handlers/callback/changeNotification.go new file mode 100644 index 0000000..c0f7954 --- /dev/null +++ b/internal/telegram/handlers/callback/changeNotification.go @@ -0,0 +1,38 @@ +package callback + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type NotificationChanger interface { + SetNotification(uid int64, isEnable bool) error + Notification(uid int64) (bool, error) +} + +func NewChangeNotification(n NotificationChanger, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewChangeNotification" + log := log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + + uid := ctx.Sender().ID + + nstat, err := n.Notification(uid) + if err != nil { + log.Warn("error while getting notification", sl.Err(err)) + return fmt.Errorf("%s get notif: %w", op, err) + } + + if err := n.SetNotification(uid, !nstat); err != nil { + log.Warn("error while set notification", sl.Err(err)) + return fmt.Errorf("%s set notif: %w", op, err) + } + + return ctx.Edit("Уведомления переключены") + } +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 3fd7d31..ae24ebb 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -3,3 +3,4 @@ package mocks //go:generate mockgen -destination=./set_stater_mock.go -package=mocks algobot/internal/telegram/handlers/text SetStater //go:generate mockgen -destination=./userInformer_mock.go -package=mocks algobot/internal/telegram/handlers/text UserInformer //go:generate mockgen -destination=./stateChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback StateChanger +//go:generate mockgen -destination=./notificationChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback NotificationChanger diff --git a/test/telegram/handlers/changeNotification_test.go b/test/telegram/handlers/changeNotification_test.go new file mode 100644 index 0000000..94b888e --- /dev/null +++ b/test/telegram/handlers/changeNotification_test.go @@ -0,0 +1,57 @@ +package test + +import ( + "algobot/internal/telegram/handlers/callback" + mocks3 "algobot/test/mocks" + mocks "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestNotification(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mctx := mocks.NewMockContext(ctrl) + notif := mocks2.NewMockNotificationChanger(ctrl) + log := mocks3.NewMockLogger() + + handler := callback.NewChangeNotification(notif, log) + mctx.EXPECT().Get("trace_id").Return("a-1").AnyTimes() + + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + notif.EXPECT().Notification(int64(1)).Return(true, nil).Times(1), + notif.EXPECT().SetNotification(int64(1), false).Return(nil).Times(1), + mctx.EXPECT().Edit("Уведомления переключены").Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("Notification err", func(t *testing.T) { + expErr := errors.New("test error") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + notif.EXPECT().Notification(int64(1)).Return(false, expErr).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, expErr) + }) + t.Run("SetNotification err", func(t *testing.T) { + expErr := errors.New("test error") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + notif.EXPECT().Notification(int64(1)).Return(true, nil).Times(1), + notif.EXPECT().SetNotification(int64(1), false).Return(expErr).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, expErr) + }) +} From 374f46b116ffe9a9ec3c6ed54580afcb156ee36a Mon Sep 17 00:00:00 2001 From: pavlov Date: Wed, 2 Apr 2025 12:41:29 +0300 Subject: [PATCH 08/44] add migrator application --- .gitignore | 2 +- Makefile | 6 +- cmd/migrator/main.go | 35 ++ go.mod | 17 +- go.sum | 74 +++-- .../telegram/keyboards/sendingCookie.go | 2 +- internal/telegram/handlers/text/ai.go | 58 ++++ migrations/01_create_table_users.sql | 12 + migrations/02_create_table_groups.sql | 12 + protos/ai.pb.go | 299 ------------------ protos/ai.proto | 26 -- protos/ai_grpc.pb.go | 159 ---------- storage/.gitkeep | 0 test/mocks/telegram/handlers/mockgen.go | 2 + test/telegram/handlers/ai_test.go | 56 ++++ 15 files changed, 232 insertions(+), 528 deletions(-) create mode 100644 cmd/migrator/main.go create mode 100644 internal/telegram/handlers/text/ai.go create mode 100644 migrations/01_create_table_users.sql create mode 100644 migrations/02_create_table_groups.sql delete mode 100644 protos/ai.pb.go delete mode 100644 protos/ai.proto delete mode 100644 protos/ai_grpc.pb.go create mode 100644 storage/.gitkeep create mode 100644 test/telegram/handlers/ai_test.go diff --git a/.gitignore b/.gitignore index 6045a68..2c5fdc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/base.db +*.db .env /.idea config/local.yaml diff --git a/Makefile b/Makefile index 46e6797..76f680b 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,8 @@ dev: .PHONY: mock-gen mock-gen: - cd test && go generate ./... \ No newline at end of file + cd test && go generate ./... + +.PHONY: migrate +migrate: + go run ./cmd/migrator/main.go -migrations-path=./migrations -storage-path=./storage/storage.db \ No newline at end of file diff --git a/cmd/migrator/main.go b/cmd/migrator/main.go new file mode 100644 index 0000000..ff54d1d --- /dev/null +++ b/cmd/migrator/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/pressly/goose/v3" +) + +func main() { + var migrationsPath, storagePath string + + flag.StringVar(&migrationsPath, "migrations-path", "", "path to migrations folder") + flag.StringVar(&storagePath, "storage-path", "", "path to storage file") + flag.Parse() + + if migrationsPath == "" { + panic("migrations-path is required") + } + + if storagePath == "" { + panic("storage-path is required") + } + + db, _ := sql.Open("sqlite3", fmt.Sprintf("file:%s", storagePath)) + + if err := goose.SetDialect("sqlite3"); err != nil { + panic(err) + } + if err := goose.Up(db, "migrations"); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index b7cd0f3..6e45ee0 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,10 @@ require ( github.com/LZTD1/telebot-router v1.1.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/ncruces/go-sqlite3 v0.25.0 + github.com/pressly/goose/v3 v3.24.2 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.71.0 - google.golang.org/protobuf v1.36.5 + go.uber.org/mock v0.5.0 gopkg.in/telebot.v4 v4.0.0-beta.4 ) @@ -16,12 +17,14 @@ require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 2a4f7b7..25e4b22 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,6 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/LZTD1/telebot-router v1.0.0 h1:Fskz8zTCfFKrrOg4LTiBgQYZXKj4nxjfVmlZRmfZzGQ= -github.com/LZTD1/telebot-router v1.0.0/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/LZTD1/telebot-router v1.1.1 h1:YNIs70tnMSjpZNFQxYhS6GcUfPBugR5FSUlJ1S3mvrc= github.com/LZTD1/telebot-router v1.1.1/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -105,6 +103,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -131,10 +131,6 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -175,8 +171,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -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.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -194,8 +188,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -302,7 +294,11 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= @@ -319,6 +315,12 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I= +github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= +github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -331,6 +333,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU= +github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -349,13 +353,18 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -380,6 +389,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -397,23 +408,13 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -435,6 +436,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -507,8 +510,6 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -541,6 +542,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -619,8 +622,8 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -632,8 +635,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -821,7 +824,6 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -853,8 +855,6 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -870,8 +870,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -900,6 +898,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA= +modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/domain/telegram/keyboards/sendingCookie.go b/internal/domain/telegram/keyboards/sendingCookie.go index 1b5f474..8714213 100644 --- a/internal/domain/telegram/keyboards/sendingCookie.go +++ b/internal/domain/telegram/keyboards/sendingCookie.go @@ -4,7 +4,7 @@ import tele "gopkg.in/telebot.v4" func RejectKeyboard() *tele.ReplyMarkup { rejectKb := &tele.ReplyMarkup{ResizeKeyboard: true} - reject := rejectKb.Text("Отменить действие") + reject := rejectKb.Text("⬅️ Назад") rejectKb.Reply( rejectKb.Row(reject), diff --git a/internal/telegram/handlers/text/ai.go b/internal/telegram/handlers/text/ai.go new file mode 100644 index 0000000..6c966de --- /dev/null +++ b/internal/telegram/handlers/text/ai.go @@ -0,0 +1,58 @@ +package text + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/lib/logger/sl" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type AIInfo struct { + TextModel string + ImageModel string +} + +type AIInformer interface { + GetAIInfo() (AIInfo, error) +} + +type AIStater interface { + SetState(uid int64, state fsm.State) +} + +func NewAI(ai AIInformer, log *slog.Logger, stater AIStater) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewAI" + + log = log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + uid := ctx.Sender().ID + + info, err := ai.GetAIInfo() + if err != nil { + log.Warn("error while GetAIInfo", sl.Err(err)) + return ctx.Send("Упс, AI сейчас не работает!") + } + + stater.SetState(uid, fsm.ChattingAI) + return ctx.Send(GetAIMessage(info), keyboards.RejectKeyboard()) + } +} + +func GetAIMessage(info AIInfo) string { + sb := strings.Builder{} + sb.WriteString("Информация о включенных моделях:\n\n") + sb.WriteString("Текст: ") + sb.WriteString(info.TextModel) + sb.WriteString("\nИзображение: ") + sb.WriteString(info.ImageModel) + sb.WriteString("\n\n") + sb.WriteString("/reset - отчистить память модели") + sb.WriteString("\n/image promt - сгенерировать изображение") + sb.WriteString("\nДля текстового запроса - просто напиши в чат") + return sb.String() +} diff --git a/migrations/01_create_table_users.sql b/migrations/01_create_table_users.sql new file mode 100644 index 0000000..6b3243c --- /dev/null +++ b/migrations/01_create_table_users.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE users +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER NOT NULL UNIQUE, + cookie TEXT DEFAULT NULL, + last_notification_msg TEXT DEFAULT NULL, + notification INTEGER DEFAULT 0 +); + +-- +goose Down +DROP TABLE users; \ No newline at end of file diff --git a/migrations/02_create_table_groups.sql b/migrations/02_create_table_groups.sql new file mode 100644 index 0000000..9df362e --- /dev/null +++ b/migrations/02_create_table_groups.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE groups +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL, + owner_id INTEGER, + title TEXT NOT NULL, + time_lesson TEXT NOT NULL +); + +-- +goose Down +DROP TABLE groups; \ No newline at end of file diff --git a/protos/ai.pb.go b/protos/ai.pb.go deleted file mode 100644 index 28edd9c..0000000 --- a/protos/ai.pb.go +++ /dev/null @@ -1,299 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.5 -// protoc v5.29.3 -// source: protos/ai.proto - -package pkg - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SuggestRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` - Suggest string `protobuf:"bytes,2,opt,name=suggest,proto3" json:"suggest,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SuggestRequest) Reset() { - *x = SuggestRequest{} - mi := &file_protos_ai_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SuggestRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SuggestRequest) ProtoMessage() {} - -func (x *SuggestRequest) ProtoReflect() protoreflect.Message { - mi := &file_protos_ai_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SuggestRequest.ProtoReflect.Descriptor instead. -func (*SuggestRequest) Descriptor() ([]byte, []int) { - return file_protos_ai_proto_rawDescGZIP(), []int{0} -} - -func (x *SuggestRequest) GetUid() int64 { - if x != nil { - return x.Uid - } - return 0 -} - -func (x *SuggestRequest) GetSuggest() string { - if x != nil { - return x.Suggest - } - return "" -} - -type SuggestResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - Request string `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SuggestResponse) Reset() { - *x = SuggestResponse{} - mi := &file_protos_ai_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SuggestResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SuggestResponse) ProtoMessage() {} - -func (x *SuggestResponse) ProtoReflect() protoreflect.Message { - mi := &file_protos_ai_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SuggestResponse.ProtoReflect.Descriptor instead. -func (*SuggestResponse) Descriptor() ([]byte, []int) { - return file_protos_ai_proto_rawDescGZIP(), []int{1} -} - -func (x *SuggestResponse) GetOk() bool { - if x != nil { - return x.Ok - } - return false -} - -func (x *SuggestResponse) GetRequest() string { - if x != nil { - return x.Request - } - return "" -} - -type ClearHistoryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ClearHistoryRequest) Reset() { - *x = ClearHistoryRequest{} - mi := &file_protos_ai_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ClearHistoryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ClearHistoryRequest) ProtoMessage() {} - -func (x *ClearHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_protos_ai_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ClearHistoryRequest.ProtoReflect.Descriptor instead. -func (*ClearHistoryRequest) Descriptor() ([]byte, []int) { - return file_protos_ai_proto_rawDescGZIP(), []int{2} -} - -func (x *ClearHistoryRequest) GetUid() int64 { - if x != nil { - return x.Uid - } - return 0 -} - -type ClearHistoryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ClearHistoryResponse) Reset() { - *x = ClearHistoryResponse{} - mi := &file_protos_ai_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ClearHistoryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ClearHistoryResponse) ProtoMessage() {} - -func (x *ClearHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_protos_ai_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ClearHistoryResponse.ProtoReflect.Descriptor instead. -func (*ClearHistoryResponse) Descriptor() ([]byte, []int) { - return file_protos_ai_proto_rawDescGZIP(), []int{3} -} - -func (x *ClearHistoryResponse) GetOk() bool { - if x != nil { - return x.Ok - } - return false -} - -var File_protos_ai_proto protoreflect.FileDescriptor - -var file_protos_ai_proto_rawDesc = string([]byte{ - 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x05, 0x70, 0x79, 0x70, 0x6b, 0x67, 0x22, 0x3c, 0x0a, 0x0e, 0x53, 0x75, 0x67, 0x67, - 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, - 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x0f, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x27, 0x0a, 0x13, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, - 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x26, 0x0a, 0x14, - 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x02, 0x6f, 0x6b, 0x32, 0x8a, 0x01, 0x0a, 0x02, 0x41, 0x69, 0x12, 0x3b, 0x0a, 0x0a, 0x47, - 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x79, 0x70, 0x6b, - 0x67, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x70, 0x79, 0x70, 0x6b, 0x67, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, - 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x2e, 0x70, 0x79, 0x70, 0x6b, 0x67, - 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x79, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x6c, 0x65, - 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x74, 0x67, 0x62, 0x6f, 0x74, 0x2f, 0x70, 0x6b, 0x67, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) - -var ( - file_protos_ai_proto_rawDescOnce sync.Once - file_protos_ai_proto_rawDescData []byte -) - -func file_protos_ai_proto_rawDescGZIP() []byte { - file_protos_ai_proto_rawDescOnce.Do(func() { - file_protos_ai_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_ai_proto_rawDesc), len(file_protos_ai_proto_rawDesc))) - }) - return file_protos_ai_proto_rawDescData -} - -var file_protos_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_protos_ai_proto_goTypes = []any{ - (*SuggestRequest)(nil), // 0: pypkg.SuggestRequest - (*SuggestResponse)(nil), // 1: pypkg.SuggestResponse - (*ClearHistoryRequest)(nil), // 2: pypkg.ClearHistoryRequest - (*ClearHistoryResponse)(nil), // 3: pypkg.ClearHistoryResponse -} -var file_protos_ai_proto_depIdxs = []int32{ - 0, // 0: pypkg.Ai.GetSuggest:input_type -> pypkg.SuggestRequest - 2, // 1: pypkg.Ai.ClearHistory:input_type -> pypkg.ClearHistoryRequest - 1, // 2: pypkg.Ai.GetSuggest:output_type -> pypkg.SuggestResponse - 3, // 3: pypkg.Ai.ClearHistory:output_type -> pypkg.ClearHistoryResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_protos_ai_proto_init() } -func file_protos_ai_proto_init() { - if File_protos_ai_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_ai_proto_rawDesc), len(file_protos_ai_proto_rawDesc)), - NumEnums: 0, - NumMessages: 4, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_protos_ai_proto_goTypes, - DependencyIndexes: file_protos_ai_proto_depIdxs, - MessageInfos: file_protos_ai_proto_msgTypes, - }.Build() - File_protos_ai_proto = out.File - file_protos_ai_proto_goTypes = nil - file_protos_ai_proto_depIdxs = nil -} diff --git a/protos/ai.proto b/protos/ai.proto deleted file mode 100644 index 876078d..0000000 --- a/protos/ai.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; - -package pypkg; -option go_package = "tgbot/pkg"; - -service Ai { - rpc GetSuggest (SuggestRequest) returns (SuggestResponse); - rpc ClearHistory (ClearHistoryRequest) returns (ClearHistoryResponse); -} - -message SuggestRequest { - int64 uid = 1; - string suggest = 2; -} - -message SuggestResponse { - bool ok = 1; - string request = 2; -} - -message ClearHistoryRequest { - int64 uid = 1; -} -message ClearHistoryResponse { - bool ok = 1; -} \ No newline at end of file diff --git a/protos/ai_grpc.pb.go b/protos/ai_grpc.pb.go deleted file mode 100644 index 4a7f2d7..0000000 --- a/protos/ai_grpc.pb.go +++ /dev/null @@ -1,159 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 -// source: protos/ai.proto - -package pkg - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - Ai_GetSuggest_FullMethodName = "/pypkg.Ai/GetSuggest" - Ai_ClearHistory_FullMethodName = "/pypkg.Ai/ClearHistory" -) - -// AiClient is the client API for Ai service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type AiClient interface { - GetSuggest(ctx context.Context, in *SuggestRequest, opts ...grpc.CallOption) (*SuggestResponse, error) - ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*ClearHistoryResponse, error) -} - -type aiClient struct { - cc grpc.ClientConnInterface -} - -func NewAiClient(cc grpc.ClientConnInterface) AiClient { - return &aiClient{cc} -} - -func (c *aiClient) GetSuggest(ctx context.Context, in *SuggestRequest, opts ...grpc.CallOption) (*SuggestResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SuggestResponse) - err := c.cc.Invoke(ctx, Ai_GetSuggest_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *aiClient) ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*ClearHistoryResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ClearHistoryResponse) - err := c.cc.Invoke(ctx, Ai_ClearHistory_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// AiServer is the server API for Ai service. -// All implementations must embed UnimplementedAiServer -// for forward compatibility. -type AiServer interface { - GetSuggest(context.Context, *SuggestRequest) (*SuggestResponse, error) - ClearHistory(context.Context, *ClearHistoryRequest) (*ClearHistoryResponse, error) - mustEmbedUnimplementedAiServer() -} - -// UnimplementedAiServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedAiServer struct{} - -func (UnimplementedAiServer) GetSuggest(context.Context, *SuggestRequest) (*SuggestResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetSuggest not implemented") -} -func (UnimplementedAiServer) ClearHistory(context.Context, *ClearHistoryRequest) (*ClearHistoryResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented") -} -func (UnimplementedAiServer) mustEmbedUnimplementedAiServer() {} -func (UnimplementedAiServer) testEmbeddedByValue() {} - -// UnsafeAiServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to AiServer will -// result in compilation errors. -type UnsafeAiServer interface { - mustEmbedUnimplementedAiServer() -} - -func RegisterAiServer(s grpc.ServiceRegistrar, srv AiServer) { - // If the following call pancis, it indicates UnimplementedAiServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&Ai_ServiceDesc, srv) -} - -func _Ai_GetSuggest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SuggestRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AiServer).GetSuggest(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Ai_GetSuggest_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AiServer).GetSuggest(ctx, req.(*SuggestRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Ai_ClearHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ClearHistoryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AiServer).ClearHistory(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Ai_ClearHistory_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AiServer).ClearHistory(ctx, req.(*ClearHistoryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// Ai_ServiceDesc is the grpc.ServiceDesc for Ai service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var Ai_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "pypkg.Ai", - HandlerType: (*AiServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "GetSuggest", - Handler: _Ai_GetSuggest_Handler, - }, - { - MethodName: "ClearHistory", - Handler: _Ai_ClearHistory_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "protos/ai.proto", -} diff --git a/storage/.gitkeep b/storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index ae24ebb..82e12c0 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -4,3 +4,5 @@ package mocks //go:generate mockgen -destination=./userInformer_mock.go -package=mocks algobot/internal/telegram/handlers/text UserInformer //go:generate mockgen -destination=./stateChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback StateChanger //go:generate mockgen -destination=./notificationChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback NotificationChanger +//go:generate mockgen -destination=./aiInformer_mock.go -package=mocks algobot/internal/telegram/handlers/text AIInformer +//go:generate mockgen -destination=./aiStater_mock.go -package=mocks algobot/internal/telegram/handlers/text AIStater diff --git a/test/telegram/handlers/ai_test.go b/test/telegram/handlers/ai_test.go new file mode 100644 index 0000000..8a4c5da --- /dev/null +++ b/test/telegram/handlers/ai_test.go @@ -0,0 +1,56 @@ +package test + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/telegram/handlers/text" + mocks3 "algobot/test/mocks" + mocks2 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestAI(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks3.NewMockLogger() + mctx := mocks2.NewMockContext(ctrl) + ai := mocks.NewMockAIInformer(ctrl) + stater := mocks.NewMockAIStater(ctrl) + + h := text.NewAI(ai, log, stater) + mctx.EXPECT().Get("trace_id").Return("a-1").AnyTimes() + + t.Run("happy path", func(t *testing.T) { + aiRet := text.AIInfo{ + TextModel: "1", + ImageModel: "1", + } + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + ai.EXPECT().GetAIInfo().Return(aiRet, nil).Times(1), + stater.EXPECT().SetState(int64(1), fsm.ChattingAI).Times(1), + mctx.EXPECT().Send(text.GetAIMessage(aiRet), keyboards.RejectKeyboard()).Times(1), + ) + err := h(mctx) + assert.NoError(t, err) + }) + t.Run("GetAIInfo return err", func(t *testing.T) { + aiRet := text.AIInfo{} + aiErr := errors.New("GetAIInfo error") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + ai.EXPECT().GetAIInfo().Return(aiRet, aiErr).Times(1), + mctx.EXPECT().Send("Упс, AI сейчас не работает!").Times(1), + ) + err := h(mctx) + assert.NoError(t, err) + }) +} From a918646ec263327c3536aad2d0cc21d50b830626 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 3 Apr 2025 10:33:05 +0300 Subject: [PATCH 09/44] add auth mw --- cmd/migrator/main.go | 5 +- internal/app/telegram/app.go | 2 + internal/config/config.go | 1 + internal/storage/sqlite/sqlite.go | 29 +++++++++ internal/storage/storage.go | 1 + internal/telegram/middleware/auth/auth.go | 44 ++++++++++++++ test/mocks/telegram/middleware/mockgen.go | 3 + test/telegram/middlewares/auth_test.go | 72 +++++++++++++++++++++++ 8 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 internal/storage/sqlite/sqlite.go create mode 100644 internal/storage/storage.go create mode 100644 internal/telegram/middleware/auth/auth.go create mode 100644 test/mocks/telegram/middleware/mockgen.go create mode 100644 test/telegram/middlewares/auth_test.go diff --git a/cmd/migrator/main.go b/cmd/migrator/main.go index ff54d1d..271e708 100644 --- a/cmd/migrator/main.go +++ b/cmd/migrator/main.go @@ -24,7 +24,10 @@ func main() { panic("storage-path is required") } - db, _ := sql.Open("sqlite3", fmt.Sprintf("file:%s", storagePath)) + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", storagePath)) + if err != nil { + panic(err) + } if err := goose.SetDialect("sqlite3"); err != nil { panic(err) diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 6d59e58..c610afa 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -6,6 +6,7 @@ import ( "algobot/internal/lib/logger/sl" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" + "algobot/internal/telegram/middleware/auth" "algobot/internal/telegram/middleware/logger" "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" @@ -55,6 +56,7 @@ func New(log *slog.Logger, token string) *App { b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) + b.Use(auth.New(nil, log)) r := router.NewRouter() diff --git a/internal/config/config.go b/internal/config/config.go index 3da393d..9e8d051 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,6 +10,7 @@ type Config struct { Env string `yaml:"env" env-default:"prod"` TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` + StoragePath string `yaml:"storage_path" env-default:"./storage/storage.db"` GRPC GRPC `yaml:"grpc"` } diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go new file mode 100644 index 0000000..83b9052 --- /dev/null +++ b/internal/storage/sqlite/sqlite.go @@ -0,0 +1,29 @@ +package sqlite + +import ( + "algobot/internal/config" + "database/sql" + "fmt" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" +) + +type Sqlite struct { + db *sql.DB +} + +func NewDB(cfg *config.Config) (*Sqlite, error) { + const op = "sqlite.NewDB" + + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", cfg.StoragePath)) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + err = db.Ping() + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &Sqlite{db: db}, nil +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1 @@ +package storage diff --git a/internal/telegram/middleware/auth/auth.go b/internal/telegram/middleware/auth/auth.go new file mode 100644 index 0000000..463e478 --- /dev/null +++ b/internal/telegram/middleware/auth/auth.go @@ -0,0 +1,44 @@ +package auth + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type Auther interface { + IsRegistered(uid int64) (bool, error) + Register(uid int64) error +} + +func New(auth Auther, log *slog.Logger) telebot.MiddlewareFunc { + return func(next telebot.HandlerFunc) telebot.HandlerFunc { + log = log.With( + slog.String("component", "middleware/auth"), + ) + + return func(ctx telebot.Context) error { + traceID := ctx.Get("trace_id") + log.With("trace_id", traceID) + + uid := ctx.Sender().ID + + isReg, err := auth.IsRegistered(uid) + if err != nil { + log.Warn("error while checking if user exists", sl.Err(err)) + return fmt.Errorf("error while checking if user exists: %w", err) + } + if !isReg { + if err := auth.Register(uid); err != nil { + log.Warn("error while register user", sl.Err(err)) + return fmt.Errorf("error while register user: %w", err) + } + + log.Info("user is registered", slog.Int64("uid", uid)) + } + + return next(ctx) + } + } +} diff --git a/test/mocks/telegram/middleware/mockgen.go b/test/mocks/telegram/middleware/mockgen.go new file mode 100644 index 0000000..2670f12 --- /dev/null +++ b/test/mocks/telegram/middleware/mockgen.go @@ -0,0 +1,3 @@ +package mocks + +//go:generate mockgen -destination=./auther_mock.go -package=mocks algobot/internal/telegram/middleware/auth Auther diff --git a/test/telegram/middlewares/auth_test.go b/test/telegram/middlewares/auth_test.go new file mode 100644 index 0000000..bbd0a7b --- /dev/null +++ b/test/telegram/middlewares/auth_test.go @@ -0,0 +1,72 @@ +package test + +import ( + "algobot/internal/telegram/middleware/auth" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/telegram" + mocks3 "algobot/test/mocks/telegram/middleware" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestAuth(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + mctx := mocks2.NewMockContext(ctrl) + auther := mocks3.NewMockAuther(ctrl) + + handler := auth.New(auther, log) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + t.Run("happy path is not registered", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + auther.EXPECT().IsRegistered(int64(1)).Return(false, nil).Times(1), + auther.EXPECT().Register(int64(1)).Return(nil).Times(1), + ) + err := handler(func(context tele.Context) error { + return nil + })(mctx) + assert.NoError(t, err) + }) + t.Run("happy path is registered", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + auther.EXPECT().IsRegistered(int64(1)).Return(true, nil).Times(1), + ) + err := handler(func(context tele.Context) error { + return nil + })(mctx) + assert.NoError(t, err) + }) + t.Run("if isReg return err", func(t *testing.T) { + errExpected := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + auther.EXPECT().IsRegistered(int64(1)).Return(false, errExpected).Times(1), + ) + err := handler(func(context tele.Context) error { + return nil + })(mctx) + assert.ErrorIs(t, err, errExpected) + }) + t.Run("if isReg return err", func(t *testing.T) { + errExpected := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + auther.EXPECT().IsRegistered(int64(1)).Return(false, nil).Times(1), + auther.EXPECT().Register(int64(1)).Return(errExpected).Times(1), + ) + err := handler(func(context tele.Context) error { + return nil + })(mctx) + assert.ErrorIs(t, err, errExpected) + }) +} From 655eca22947868d2294c90c9b261feed1ef8aa3a Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 3 Apr 2025 14:18:28 +0300 Subject: [PATCH 10/44] realization sqlite base and 2 methods + tests --- internal/app/app.go | 7 +- internal/app/telegram/app.go | 7 +- internal/storage/sqlite/isRegistered.go | 23 +++++++ internal/storage/sqlite/register.go | 26 +++++++ internal/storage/sqlite/sqlite.go | 10 +++ internal/storage/storage.go | 6 ++ internal/telegram/middleware/auth/auth.go | 2 +- .../01_create_table_users.sql | 12 ++++ .../02_create_table_groups.sql | 12 ++++ .../03_append_user_in_users_table.sql | 9 +++ test/storage/sqlite_test.go | 69 +++++++++++++++++++ 11 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 internal/storage/sqlite/isRegistered.go create mode 100644 internal/storage/sqlite/register.go create mode 100644 test/storage/migrations-suite/01_create_table_users.sql create mode 100644 test/storage/migrations-suite/02_create_table_groups.sql create mode 100644 test/storage/migrations-suite/03_append_user_in_users_table.sql create mode 100644 test/storage/sqlite_test.go diff --git a/internal/app/app.go b/internal/app/app.go index d85261b..01eb6ef 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,6 +3,7 @@ package app import ( "algobot/internal/app/telegram" "algobot/internal/config" + "algobot/internal/storage/sqlite" "log/slog" ) @@ -14,7 +15,11 @@ type App struct { func New(log *slog.Logger, cfg *config.Config) *App { - botApplication := telegram.New(log, cfg.TelegramToken) + storage, err := sqlite.NewDB(cfg) + if err != nil { + panic(err) + } + botApplication := telegram.New(log, cfg.TelegramToken, storage) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index c610afa..6687711 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -25,7 +25,7 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string) *App { +func New(log *slog.Logger, token string, auther auth.Auther) *App { const op = "telegram.New" nlog := log.With( @@ -40,7 +40,7 @@ func New(log *slog.Logger, token string) *App { OnError: func(e error, c tele.Context) { // TODO : refactor into handler traceID := c.Get("trace_id") // TODO : maybe send warnings to admin ? c.Send(fmt.Sprintf("[%s]\n\nУпс! Произошла какая-то непредвиденная ошибка!\nОбратитесь к администратору", traceID), tele.ModeHTML) - log.Warn("can`t handle error", sl.Err(e), slog.Any("trace_id", traceID)) + log.Warn("cant handle error", sl.Err(e), slog.Any("trace_id", traceID)) }, } b, err := tele.NewBot(pref) @@ -56,7 +56,7 @@ func New(log *slog.Logger, token string) *App { b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) - b.Use(auth.New(nil, log)) + b.Use(auth.New(auther, log)) r := router.NewRouter() @@ -75,6 +75,7 @@ func New(log *slog.Logger, token string) *App { r.Group(func(r router.Router) { // Routes for SendingCookie state r.Use(stater.New(stateMachine, fsm.SendingCookie)) + // text r.HandleFuncText("Отменить действие", text.NewStart(stateMachine)) }) diff --git a/internal/storage/sqlite/isRegistered.go b/internal/storage/sqlite/isRegistered.go new file mode 100644 index 0000000..de39484 --- /dev/null +++ b/internal/storage/sqlite/isRegistered.go @@ -0,0 +1,23 @@ +package sqlite + +import "fmt" + +func (s *Sqlite) IsRegistered(uid int64) (bool, error) { + const op = "sqlite.IsRegistered" + + sql := `SELECT COUNT(*) FROM users WHERE uid = ?` + + pr, err := s.db.Prepare(sql) + if err != nil { + return false, fmt.Errorf("%s error while preparing sql: %w", op, err) + } + + var count int + + row := pr.QueryRow(uid) + if err := row.Scan(&count); err != nil { + return false, fmt.Errorf("%s error while scanning sql: %w", op, err) + } + + return count > 0, nil +} diff --git a/internal/storage/sqlite/register.go b/internal/storage/sqlite/register.go new file mode 100644 index 0000000..74a76c2 --- /dev/null +++ b/internal/storage/sqlite/register.go @@ -0,0 +1,26 @@ +package sqlite + +import ( + "fmt" +) + +func (s *Sqlite) Register(uid int64) error { + const op = "sqlite.Register" + + sqle := ` + INSERT INTO users (uid, cookie, last_notification_msg, notification) + VALUES (?, NULL, NULL, 0); + ` + + pr, err := s.db.Prepare(sqle) + if err != nil { + return fmt.Errorf("%s error while preparing sql: %w", op, err) + } + + _, err = pr.Exec(uid) + if err != nil { + return fmt.Errorf("%s error while exec sql: %w", op, err) + } + + return nil +} diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 83b9052..66b8429 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -27,3 +27,13 @@ func NewDB(cfg *config.Config) (*Sqlite, error) { return &Sqlite{db: db}, nil } + +func (s *Sqlite) MustClose() { + if err := s.Close(); err != nil { + panic(err) + } +} + +func (s *Sqlite) Close() error { + return s.db.Close() +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 82be054..cfb5874 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -1 +1,7 @@ package storage + +import "errors" + +var ( + ErrAlreadyExists = errors.New("already exists") +) diff --git a/internal/telegram/middleware/auth/auth.go b/internal/telegram/middleware/auth/auth.go index 463e478..6210b14 100644 --- a/internal/telegram/middleware/auth/auth.go +++ b/internal/telegram/middleware/auth/auth.go @@ -35,7 +35,7 @@ func New(auth Auther, log *slog.Logger) telebot.MiddlewareFunc { return fmt.Errorf("error while register user: %w", err) } - log.Info("user is registered", slog.Int64("uid", uid)) + log.Info("new registration", slog.Int64("uid", uid)) } return next(ctx) diff --git a/test/storage/migrations-suite/01_create_table_users.sql b/test/storage/migrations-suite/01_create_table_users.sql new file mode 100644 index 0000000..6b3243c --- /dev/null +++ b/test/storage/migrations-suite/01_create_table_users.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE users +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER NOT NULL UNIQUE, + cookie TEXT DEFAULT NULL, + last_notification_msg TEXT DEFAULT NULL, + notification INTEGER DEFAULT 0 +); + +-- +goose Down +DROP TABLE users; \ No newline at end of file diff --git a/test/storage/migrations-suite/02_create_table_groups.sql b/test/storage/migrations-suite/02_create_table_groups.sql new file mode 100644 index 0000000..9df362e --- /dev/null +++ b/test/storage/migrations-suite/02_create_table_groups.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE groups +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL, + owner_id INTEGER, + title TEXT NOT NULL, + time_lesson TEXT NOT NULL +); + +-- +goose Down +DROP TABLE groups; \ No newline at end of file diff --git a/test/storage/migrations-suite/03_append_user_in_users_table.sql b/test/storage/migrations-suite/03_append_user_in_users_table.sql new file mode 100644 index 0000000..d55dfbf --- /dev/null +++ b/test/storage/migrations-suite/03_append_user_in_users_table.sql @@ -0,0 +1,9 @@ +-- +goose Up +INSERT INTO users (uid, cookie, last_notification_msg, notification) +VALUES (1001, 'cookie', NULL, 0); + + +-- +goose Down +DELETE +FROM users +WHERE uid = 1001 \ No newline at end of file diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go new file mode 100644 index 0000000..0ddbc6a --- /dev/null +++ b/test/storage/sqlite_test.go @@ -0,0 +1,69 @@ +package test + +import ( + "algobot/internal/config" + sqlite2 "algobot/internal/storage/sqlite" + "database/sql" + "fmt" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/pressly/goose/v3" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +const ( + testDBPath = "test.db" + migrationsPath = "./migrations-suite" +) + +func TestSqlite(t *testing.T) { + t.Cleanup(func() { + err := os.Remove(testDBPath) + if err != nil { + panic(err) + } + }) + + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", testDBPath)) + defer db.Close() + assert.NoError(t, err) + + err = goose.SetDialect("sqlite3") + assert.NoError(t, err) + + err = goose.Up(db, migrationsPath) + assert.NoError(t, err) + + sqlite, err := sqlite2.NewDB(&config.Config{ + StoragePath: testDBPath, + }) + defer sqlite.MustClose() + t.Run("IsRegistered", func(t *testing.T) { + t.Run("user have reg", func(t *testing.T) { + registered, err := sqlite.IsRegistered(1001) + assert.NoError(t, err) + assert.True(t, registered) + }) + t.Run("user dont have reg", func(t *testing.T) { + registered, err := sqlite.IsRegistered(0) + assert.NoError(t, err) + assert.False(t, registered) + }) + }) + t.Run("Register", func(t *testing.T) { + t.Run("successful registered", func(t *testing.T) { + err := sqlite.Register(1002) + assert.NoError(t, err) + + registered, err := sqlite.IsRegistered(1002) + assert.NoError(t, err) + assert.True(t, registered) + }) + t.Run("already registered", func(t *testing.T) { + err := sqlite.Register(1001) + assert.Error(t, err) + }) + }) +} From 88bc6c70abcb84b159f8e6199e523df198f8b597 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 3 Apr 2025 17:14:48 +0300 Subject: [PATCH 11/44] add cookies and notification methods --- internal/app/app.go | 2 +- internal/app/telegram/app.go | 54 ++++++++++++++----- internal/storage/sqlite/cookies.go | 29 ++++++++++ internal/storage/sqlite/notification.go | 24 +++++++++ internal/telegram/middleware/stater/stater.go | 5 ++ .../03_append_user_in_users_table.sql | 5 +- test/storage/sqlite_test.go | 18 +++++++ 7 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 internal/storage/sqlite/cookies.go create mode 100644 internal/storage/sqlite/notification.go diff --git a/internal/app/app.go b/internal/app/app.go index 01eb6ef..2c49da9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,7 +19,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } - botApplication := telegram.New(log, cfg.TelegramToken, storage) + botApplication := telegram.New(log, cfg.TelegramToken, storage, storage) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 6687711..3e524d1 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -8,9 +8,7 @@ import ( "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" "algobot/internal/telegram/middleware/logger" - "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" - "errors" "fmt" router "github.com/LZTD1/telebot-router" tele "gopkg.in/telebot.v4" @@ -25,7 +23,7 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string, auther auth.Auther) *App { +func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer) *App { const op = "telegram.New" nlog := log.With( @@ -59,29 +57,59 @@ func New(log *slog.Logger, token string, auther auth.Auther) *App { b.Use(auth.New(auther, log)) r := router.NewRouter() - + //st := stater.New(stateMachine, fsm.Default) r.Group(func(r router.Router) { // Routes for default state - r.Use(stater.New(stateMachine, fsm.Default)) + r.Use(func(handler router.RouteHandler) router.RouteHandler { + return router.HandlerFunc(func(ctx tele.Context) error { + fmt.Printf("1) ") + fmt.Printf("username: %s ", ctx.Sender().Username) + fmt.Printf("message: %s ", ctx.Message().Text) + fmt.Printf("onState: %d ", fsm.Default) + fmt.Printf("stater.State: %d \n", stateMachine.State(ctx.Sender().ID)) + if stateMachine.State(ctx.Sender().ID) == fsm.Default { + return handler.ServeContext(ctx) + } + + return nil + }) + }) // message r.HandleFuncText("/start", text.NewStart(stateMachine)) - r.HandleFuncText("Настройки", text.NewSettings(nil, nil)) + r.HandleFuncText("Настройки", text.NewSettings(set, log)) + //r.HandleFuncText(".", text.NewStart(stateMachine)) // callbacks - r.HandleFuncCallback("set_cookie", callback.NewChangeCookie(nil)) + r.HandleFuncCallback("set_cookie", callback.NewChangeCookie(stateMachine)) r.HandleFuncCallback("change_notification", nil) }) r.Group(func(r router.Router) { // Routes for SendingCookie state - r.Use(stater.New(stateMachine, fsm.SendingCookie)) + r.Use(func(handler router.RouteHandler) router.RouteHandler { + return router.HandlerFunc(func(ctx tele.Context) error { + fmt.Printf("2) ") + fmt.Printf("username: %s ", ctx.Sender().Username) + fmt.Printf("message: %s ", ctx.Message().Text) + fmt.Printf("onState: %d ", fsm.Default) + fmt.Printf("stater.State: %d \n", stateMachine.State(ctx.Sender().ID)) + + if stateMachine.State(ctx.Sender().ID) == fsm.SendingCookie { + return handler.ServeContext(ctx) + } + + return nil + }) + }) - // text - r.HandleFuncText("Отменить действие", text.NewStart(stateMachine)) + // message + r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) + r.HandleFuncText(".", func(c tele.Context) error { + return c.Reply("accepted") + }) }) - b.Handle(tele.OnText, func(context tele.Context) error { - return errors.New("error") - }) + b.Handle(tele.OnText, r.ServeContext) + b.Handle(tele.OnCallback, r.ServeContext) return &App{log: log, bot: b} } diff --git a/internal/storage/sqlite/cookies.go b/internal/storage/sqlite/cookies.go new file mode 100644 index 0000000..506db01 --- /dev/null +++ b/internal/storage/sqlite/cookies.go @@ -0,0 +1,29 @@ +package sqlite + +import ( + "database/sql" + "fmt" +) + +func (s *Sqlite) Cookies(uid int64) (string, error) { + const op = "sqlite.Cookies" + + q := `SELECT cookie FROM main.users WHERE uid=?` + pr, err := s.db.Prepare(q) + if err != nil { + return "", fmt.Errorf("%s error while preparing sql: %w", op, err) + } + + var cookie sql.NullString + + row := pr.QueryRow(uid) + if err := row.Scan(&cookie); err != nil { + return "", fmt.Errorf("%s error while scanning sql: %w", op, err) + } + + if !cookie.Valid { + return "", nil + } + + return cookie.String, nil +} diff --git a/internal/storage/sqlite/notification.go b/internal/storage/sqlite/notification.go new file mode 100644 index 0000000..88b1555 --- /dev/null +++ b/internal/storage/sqlite/notification.go @@ -0,0 +1,24 @@ +package sqlite + +import ( + "fmt" +) + +func (s *Sqlite) Notification(uid int64) (bool, error) { + const op = "sqlite.Notification" + + q := `SELECT notification FROM main.users WHERE uid=?` + pr, err := s.db.Prepare(q) + if err != nil { + return false, fmt.Errorf("%s error while preparing sql: %w", op, err) + } + + var notification int + + row := pr.QueryRow(uid) + if err := row.Scan(¬ification); err != nil { + return false, fmt.Errorf("%s error while scanning sql: %w", op, err) + } + + return notification == 1, nil +} diff --git a/internal/telegram/middleware/stater/stater.go b/internal/telegram/middleware/stater/stater.go index 6710dd5..802c9b9 100644 --- a/internal/telegram/middleware/stater/stater.go +++ b/internal/telegram/middleware/stater/stater.go @@ -2,6 +2,7 @@ package stater import ( "algobot/internal/lib/fsm" + "fmt" router "github.com/LZTD1/telebot-router" "gopkg.in/telebot.v4" ) @@ -13,6 +14,10 @@ type Stater interface { func New(stater Stater, onState fsm.State) func(next router.RouteHandler) router.RouteHandler { return func(next router.RouteHandler) router.RouteHandler { return router.HandlerFunc(func(ctx telebot.Context) error { + fmt.Printf("username: %s ", ctx.Sender().Username) + fmt.Printf("message: %s ", ctx.Message().Text) + fmt.Printf("onState: %d ", onState) + fmt.Printf("stater.State: %d \n", stater.State(ctx.Sender().ID)) if stater.State(ctx.Sender().ID) == onState { return next.ServeContext(ctx) } diff --git a/test/storage/migrations-suite/03_append_user_in_users_table.sql b/test/storage/migrations-suite/03_append_user_in_users_table.sql index d55dfbf..a511876 100644 --- a/test/storage/migrations-suite/03_append_user_in_users_table.sql +++ b/test/storage/migrations-suite/03_append_user_in_users_table.sql @@ -1,9 +1,10 @@ -- +goose Up INSERT INTO users (uid, cookie, last_notification_msg, notification) -VALUES (1001, 'cookie', NULL, 0); +VALUES (1001, 'cookie', NULL, 0), + (1000, NULL, NULL, 0); -- +goose Down DELETE FROM users -WHERE uid = 1001 \ No newline at end of file +WHERE uid in (1001, 1002) \ No newline at end of file diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index 0ddbc6a..b05bc9a 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -40,6 +40,7 @@ func TestSqlite(t *testing.T) { StoragePath: testDBPath, }) defer sqlite.MustClose() + t.Run("IsRegistered", func(t *testing.T) { t.Run("user have reg", func(t *testing.T) { registered, err := sqlite.IsRegistered(1001) @@ -66,4 +67,21 @@ func TestSqlite(t *testing.T) { assert.Error(t, err) }) }) + t.Run("Cookie", func(t *testing.T) { + t.Run("Cookie set", func(t *testing.T) { + cookie, err := sqlite.Cookies(1000) + assert.NoError(t, err) + assert.Equal(t, "", cookie) + }) + t.Run("Cookie null", func(t *testing.T) { + cookie, err := sqlite.Cookies(1001) + assert.NoError(t, err) + assert.Equal(t, "cookie", cookie) + }) + }) + t.Run("Notification", func(t *testing.T) { + notfication, err := sqlite.Notification(1000) + assert.NoError(t, err) + assert.Equal(t, false, notfication) + }) } From d81c5c28d2c8a812368be6ed74c7577d5cb0917a Mon Sep 17 00:00:00 2001 From: danil227pavlov Date: Thu, 3 Apr 2025 21:25:40 +0300 Subject: [PATCH 12/44] migrate to new telebot router --- config/dev.yaml | 2 +- go.mod | 2 +- go.sum | 2 + internal/app/telegram/app.go | 44 ++++--------------- internal/telegram/middleware/stater/stater.go | 7 +-- 5 files changed, 13 insertions(+), 44 deletions(-) diff --git a/config/dev.yaml b/config/dev.yaml index 1d950d0..59a5ceb 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -1,5 +1,5 @@ env: local # or prod -storage_path: "./storage/base.db" +storage_path: "./storage/storage.db" # telegram token set in env variables - TELEGRAM_TOKEN migrations_path: "./migrations" grpc: diff --git a/go.mod b/go.mod index 6e45ee0..4404c3b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module algobot go 1.23.3 require ( - github.com/LZTD1/telebot-router v1.1.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/ncruces/go-sqlite3 v0.25.0 @@ -15,6 +14,7 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect + github.com/LZTD1/telebot-context-router v1.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect diff --git a/go.sum b/go.sum index 25e4b22..777dfda 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/LZTD1/telebot-context-router v1.0.1 h1:iGrKhFevnXFtQre+3bum6g9AluMTE23IVz2A+cOZPYE= +github.com/LZTD1/telebot-context-router v1.0.1/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= github.com/LZTD1/telebot-router v1.1.1 h1:YNIs70tnMSjpZNFQxYhS6GcUfPBugR5FSUlJ1S3mvrc= github.com/LZTD1/telebot-router v1.1.1/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 3e524d1..3d557a5 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -8,9 +8,10 @@ import ( "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" "algobot/internal/telegram/middleware/logger" + "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" "fmt" - router "github.com/LZTD1/telebot-router" + router "github.com/LZTD1/telebot-context-router" tele "gopkg.in/telebot.v4" "gopkg.in/telebot.v4/middleware" "log/slog" @@ -59,55 +60,26 @@ func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInform r := router.NewRouter() //st := stater.New(stateMachine, fsm.Default) r.Group(func(r router.Router) { // Routes for default state - r.Use(func(handler router.RouteHandler) router.RouteHandler { - return router.HandlerFunc(func(ctx tele.Context) error { - fmt.Printf("1) ") - fmt.Printf("username: %s ", ctx.Sender().Username) - fmt.Printf("message: %s ", ctx.Message().Text) - fmt.Printf("onState: %d ", fsm.Default) - fmt.Printf("stater.State: %d \n", stateMachine.State(ctx.Sender().ID)) - if stateMachine.State(ctx.Sender().ID) == fsm.Default { - return handler.ServeContext(ctx) - } - - return nil - }) - }) + r.Use(stater.New(stateMachine, fsm.Default)) // message r.HandleFuncText("/start", text.NewStart(stateMachine)) r.HandleFuncText("Настройки", text.NewSettings(set, log)) - //r.HandleFuncText(".", text.NewStart(stateMachine)) // callbacks - r.HandleFuncCallback("set_cookie", callback.NewChangeCookie(stateMachine)) - r.HandleFuncCallback("change_notification", nil) + r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) + r.HandleFuncCallback("\fchange_notification", nil) }) r.Group(func(r router.Router) { // Routes for SendingCookie state - r.Use(func(handler router.RouteHandler) router.RouteHandler { - return router.HandlerFunc(func(ctx tele.Context) error { - fmt.Printf("2) ") - fmt.Printf("username: %s ", ctx.Sender().Username) - fmt.Printf("message: %s ", ctx.Message().Text) - fmt.Printf("onState: %d ", fsm.Default) - fmt.Printf("stater.State: %d \n", stateMachine.State(ctx.Sender().ID)) - - if stateMachine.State(ctx.Sender().ID) == fsm.SendingCookie { - return handler.ServeContext(ctx) - } - - return nil - }) - }) + r.Use(stater.New(stateMachine, fsm.SendingCookie)) // message r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) - r.HandleFuncText(".", func(c tele.Context) error { - return c.Reply("accepted") - }) }) + r.NotFound(text.NewStart(stateMachine)) + b.Handle(tele.OnText, r.ServeContext) b.Handle(tele.OnCallback, r.ServeContext) diff --git a/internal/telegram/middleware/stater/stater.go b/internal/telegram/middleware/stater/stater.go index 802c9b9..64e636d 100644 --- a/internal/telegram/middleware/stater/stater.go +++ b/internal/telegram/middleware/stater/stater.go @@ -2,8 +2,7 @@ package stater import ( "algobot/internal/lib/fsm" - "fmt" - router "github.com/LZTD1/telebot-router" + router "github.com/LZTD1/telebot-context-router" "gopkg.in/telebot.v4" ) @@ -14,10 +13,6 @@ type Stater interface { func New(stater Stater, onState fsm.State) func(next router.RouteHandler) router.RouteHandler { return func(next router.RouteHandler) router.RouteHandler { return router.HandlerFunc(func(ctx telebot.Context) error { - fmt.Printf("username: %s ", ctx.Sender().Username) - fmt.Printf("message: %s ", ctx.Message().Text) - fmt.Printf("onState: %d ", onState) - fmt.Printf("stater.State: %d \n", stater.State(ctx.Sender().ID)) if stater.State(ctx.Sender().ID) == onState { return next.ServeContext(ctx) } From bb9a53fc6be634cb14212d5e69c55d25588b5777 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 09:28:18 +0300 Subject: [PATCH 13/44] add setCookie method and handler --- internal/app/app.go | 2 +- internal/app/telegram/app.go | 4 +- internal/storage/sqlite/setCookie.go | 20 +++++++ .../telegram/handlers/text/sendingCookie.go | 40 +++++++++++++ test/mocks/telegram/handlers/mockgen.go | 3 + .../03_append_user_in_users_table.sql | 3 +- test/storage/sqlite_test.go | 12 ++++ test/telegram/handlers/sendingCookie_test.go | 56 +++++++++++++++++++ 8 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 internal/storage/sqlite/setCookie.go create mode 100644 internal/telegram/handlers/text/sendingCookie.go create mode 100644 test/telegram/handlers/sendingCookie_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 2c49da9..4e48d7e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,7 +19,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } - botApplication := telegram.New(log, cfg.TelegramToken, storage, storage) + botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 3d557a5..4fadab8 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -16,6 +16,7 @@ import ( "gopkg.in/telebot.v4/middleware" "log/slog" "os" + "regexp" "time" ) @@ -24,7 +25,7 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer) *App { +func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter) *App { const op = "telegram.New" nlog := log.With( @@ -76,6 +77,7 @@ func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInform // message r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) + r.HandleFuncRegexpText(regexp.MustCompile(".+"), text.NewSendingCookie(log, cookieSetter, stateMachine)) }) r.NotFound(text.NewStart(stateMachine)) diff --git a/internal/storage/sqlite/setCookie.go b/internal/storage/sqlite/setCookie.go new file mode 100644 index 0000000..0b0eb07 --- /dev/null +++ b/internal/storage/sqlite/setCookie.go @@ -0,0 +1,20 @@ +package sqlite + +import "fmt" + +func (s *Sqlite) SetCookie(uid int64, cookie string) error { + const op = "sqlite.SetCookie" + + sqlq := "UPDATE users SET cookie=? WHERE uid=?" + pr, err := s.db.Prepare(sqlq) + if err != nil { + return fmt.Errorf("%s error while preparing statement: %w", op, err) + } + defer pr.Close() + + if _, err := pr.Exec(cookie, uid); err != nil { + return fmt.Errorf("%s error while executing statement: %w", op, err) + } + + return nil +} diff --git a/internal/telegram/handlers/text/sendingCookie.go b/internal/telegram/handlers/text/sendingCookie.go new file mode 100644 index 0000000..95a87a3 --- /dev/null +++ b/internal/telegram/handlers/text/sendingCookie.go @@ -0,0 +1,40 @@ +package text + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type CookieSetter interface { + SetCookie(uid int64, cookie string) error +} + +type CookieStater interface { + SetState(uid int64, state fsm.State) +} + +func NewSendingCookie(log *slog.Logger, cookieSetter CookieSetter, stater CookieStater) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewSendingCookie" + + log = log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + + uid := ctx.Sender().ID + cookie := ctx.Message().Text + + if err := cookieSetter.SetCookie(uid, cookie); err != nil { + log.Warn("error while setting cookie", sl.Err(err)) + return fmt.Errorf("%s error while setting cookie: %w", op, err) + } + + stater.SetState(uid, fsm.Default) + return ctx.Send("Cookie успешно установлены", keyboards.Start()) + } +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 82e12c0..99e389c 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -6,3 +6,6 @@ package mocks //go:generate mockgen -destination=./notificationChanger_mock.go -package=mocks algobot/internal/telegram/handlers/callback NotificationChanger //go:generate mockgen -destination=./aiInformer_mock.go -package=mocks algobot/internal/telegram/handlers/text AIInformer //go:generate mockgen -destination=./aiStater_mock.go -package=mocks algobot/internal/telegram/handlers/text AIStater + +//go:generate mockgen -destination=./cookieSetter_mock.go -package=mocks algobot/internal/telegram/handlers/text CookieSetter +//go:generate mockgen -destination=./cookieStater_mock.go -package=mocks algobot/internal/telegram/handlers/text CookieStater diff --git a/test/storage/migrations-suite/03_append_user_in_users_table.sql b/test/storage/migrations-suite/03_append_user_in_users_table.sql index a511876..5880277 100644 --- a/test/storage/migrations-suite/03_append_user_in_users_table.sql +++ b/test/storage/migrations-suite/03_append_user_in_users_table.sql @@ -1,7 +1,8 @@ -- +goose Up INSERT INTO users (uid, cookie, last_notification_msg, notification) VALUES (1001, 'cookie', NULL, 0), - (1000, NULL, NULL, 0); + (1000, NULL, NULL, 0), + (999, NULL, NULL, 0); -- +goose Down diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index b05bc9a..6b6ca2b 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -84,4 +84,16 @@ func TestSqlite(t *testing.T) { assert.NoError(t, err) assert.Equal(t, false, notfication) }) + t.Run("SetCookie", func(t *testing.T) { + cookie, err := sqlite.Cookies(999) + assert.NoError(t, err) + assert.Equal(t, "", cookie) + + err = sqlite.SetCookie(999, "a@a") + assert.NoError(t, err) + + cookie, err = sqlite.Cookies(999) + assert.NoError(t, err) + assert.Equal(t, "a@a", cookie) + }) } diff --git a/test/telegram/handlers/sendingCookie_test.go b/test/telegram/handlers/sendingCookie_test.go new file mode 100644 index 0000000..966e3bc --- /dev/null +++ b/test/telegram/handlers/sendingCookie_test.go @@ -0,0 +1,56 @@ +package test + +import ( + "algobot/internal/domain/telegram/keyboards" + "algobot/internal/lib/fsm" + "algobot/internal/telegram/handlers/text" + "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestSendingCookie(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + stater := mocks2.NewMockCookieStater(ctrl) + setter := mocks2.NewMockCookieSetter(ctrl) + mctx := mocks3.NewMockContext(ctrl) + + handler := text.NewSendingCookie(log, setter, stater) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + t.Run("happy path", func(t *testing.T) { + cookieText := "ckc" + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: cookieText}).Times(1), + setter.EXPECT().SetCookie(int64(1), cookieText).Return(nil).Times(1), + stater.EXPECT().SetState(int64(1), fsm.Default).Times(1), + mctx.EXPECT().Send("Cookie успешно установлены", keyboards.Start()).Return(nil).Times(1), + ) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("SetCookie return err", func(t *testing.T) { + cookieText := "ckc" + errCookie := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: cookieText}).Times(1), + setter.EXPECT().SetCookie(int64(1), cookieText).Return(errCookie).Times(1), + ) + + err := handler(mctx) + assert.ErrorIs(t, err, errCookie) + }) +} From 9e4f62fec3b88a8d9cae9b3d546024c48ca60773 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 09:34:56 +0300 Subject: [PATCH 14/44] add set notification method + tests --- internal/app/app.go | 2 +- internal/app/telegram/app.go | 4 ++-- internal/storage/sqlite/setNotification.go | 25 ++++++++++++++++++++++ test/storage/sqlite_test.go | 19 ++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 internal/storage/sqlite/setNotification.go diff --git a/internal/app/app.go b/internal/app/app.go index 4e48d7e..213f4f6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,7 +19,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } - botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage) + botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage, storage) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 4fadab8..3a7ed30 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -25,7 +25,7 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter) *App { +func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter, notifChanger callback.NotificationChanger) *App { const op = "telegram.New" nlog := log.With( @@ -69,7 +69,7 @@ func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInform // callbacks r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) - r.HandleFuncCallback("\fchange_notification", nil) + r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(notifChanger, log)) }) r.Group(func(r router.Router) { // Routes for SendingCookie state diff --git a/internal/storage/sqlite/setNotification.go b/internal/storage/sqlite/setNotification.go new file mode 100644 index 0000000..4a0d814 --- /dev/null +++ b/internal/storage/sqlite/setNotification.go @@ -0,0 +1,25 @@ +package sqlite + +import "fmt" + +func (s *Sqlite) SetNotification(uid int64, isEnable bool) error { + const op = "sqlite.SetNotification" + + digit := 0 + if isEnable { + digit = 1 + } + + sqlq := "UPDATE users SET notification = ? WHERE uid = ?" + pr, err := s.db.Prepare(sqlq) + if err != nil { + return fmt.Errorf("%s error while preparing statement: %w", op, err) + } + defer pr.Close() + + if _, err := pr.Exec(digit, uid); err != nil { + return fmt.Errorf("%s error while executing statement: %w", op, err) + } + + return nil +} diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index 6b6ca2b..4554048 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -96,4 +96,23 @@ func TestSqlite(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "a@a", cookie) }) + t.Run("SetNotification", func(t *testing.T) { + notif, err := sqlite.Notification(999) + assert.NoError(t, err) + assert.False(t, notif) + + err = sqlite.SetNotification(999, true) + assert.NoError(t, err) + + notif, err = sqlite.Notification(999) + assert.NoError(t, err) + assert.True(t, notif) + + err = sqlite.SetNotification(999, false) + assert.NoError(t, err) + + notif, err = sqlite.Notification(999) + assert.NoError(t, err) + assert.False(t, notif) + }) } From e154cd356fc3b9c89c4a8ee73a977a2670466d92 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 10:30:15 +0300 Subject: [PATCH 15/44] add my groups handler + tests --- internal/domain/models/group.go | 9 ++ internal/telegram/handlers/text/myGroups.go | 111 ++++++++++++++++++++ test/mocks/telegram/handlers/mockgen.go | 3 + test/telegram/handlers/myGroups_test.go | 86 +++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 internal/domain/models/group.go create mode 100644 internal/telegram/handlers/text/myGroups.go create mode 100644 test/telegram/handlers/myGroups_test.go diff --git a/internal/domain/models/group.go b/internal/domain/models/group.go new file mode 100644 index 0000000..2589b2a --- /dev/null +++ b/internal/domain/models/group.go @@ -0,0 +1,9 @@ +package models + +import "time" + +type Group struct { + GroupID int + Title string + TimeLesson time.Time +} diff --git a/internal/telegram/handlers/text/myGroups.go b/internal/telegram/handlers/text/myGroups.go new file mode 100644 index 0000000..f791a1c --- /dev/null +++ b/internal/telegram/handlers/text/myGroups.go @@ -0,0 +1,111 @@ +package text + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" + "time" +) + +var locales = map[time.Weekday]string{ + time.Monday: "пн", + time.Tuesday: "вт", + time.Wednesday: "ср", + time.Thursday: "чт", + time.Friday: "пт", + time.Saturday: "сб", + time.Sunday: "вс", +} + +type Grouper interface { + Groups(uid int64) ([]models.Group, error) +} + +type GroupSerializer interface { + Serialize(group models.Group) (string, error) +} +type MyGroup struct { + log *slog.Logger + grouper Grouper + serializer GroupSerializer + botName string +} + +func NewMyGroup(log *slog.Logger, grouper Grouper, serializer GroupSerializer, name string) *MyGroup { + return &MyGroup{ + log: log, + grouper: grouper, + serializer: serializer, + botName: name, + } +} + +func (g *MyGroup) ServeContext(ctx telebot.Context) error { + const op = "text.MyGroup.ServeContext" + log := g.log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + + uid := ctx.Sender().ID + groups, err := g.grouper.Groups(uid) + if err != nil { + log.Warn("error while getting groups", sl.Err(err)) + return ctx.Send(fmt.Sprintf("[%s] Ошибка при получении групп!", ctx.Get("trace_id")), telebot.ModeHTML) + } + + return ctx.Send(g.msgMyGroups(groups, ctx), telebot.ModeHTML) +} + +func (g *MyGroup) msgMyGroups(groups []models.Group, ctx telebot.Context) string { + s := &strings.Builder{} + s.WriteString(fmt.Sprintf("Всего групп: %d\n", len(groups))) + + if len(groups) == 0 { + s.WriteString("Попробуйте обновить группы!") + return s.String() + } + + beforeDay := groups[0].TimeLesson.Weekday() + c := 1 + for _, group := range groups { + if beforeDay != group.TimeLesson.Weekday() { + c = 1 + beforeDay = group.TimeLesson.Weekday() + s.WriteString("\n") + } + s.WriteString("\n") + s.WriteString(fmt.Sprintf( + "%d. %s 🕐 %s %s", + c, + g.getFormattedTitle(group, ctx), + g.getLocale(group.TimeLesson), + group.TimeLesson.Format("15:04"), + )) + c += 1 + } + + return s.String() +} + +func (g *MyGroup) getFormattedTitle(group models.Group, ctx telebot.Context) string { + const op = "text.MyGroup.getFormattedTitle" + log := g.log.With( + slog.String("op", op), + slog.Any("trace_id", ctx.Get("trace_id")), + ) + + serialized, err := g.serializer.Serialize(group) + if err != nil { + log.Warn("error while serializing group", sl.Err(err)) + return group.Title + } + return fmt.Sprintf("[%s](t.me/%s?start=%s)", group.Title, g.botName, serialized) +} + +func (g *MyGroup) getLocale(t time.Time) string { + return locales[t.Weekday()] +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 99e389c..48a45f0 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -9,3 +9,6 @@ package mocks //go:generate mockgen -destination=./cookieSetter_mock.go -package=mocks algobot/internal/telegram/handlers/text CookieSetter //go:generate mockgen -destination=./cookieStater_mock.go -package=mocks algobot/internal/telegram/handlers/text CookieStater + +//go:generate mockgen -destination=./grouper_mock.go -package=mocks algobot/internal/telegram/handlers/text Grouper +//go:generate mockgen -destination=./groupSerializer_mock.go -package=mocks algobot/internal/telegram/handlers/text GroupSerializer diff --git a/test/telegram/handlers/myGroups_test.go b/test/telegram/handlers/myGroups_test.go new file mode 100644 index 0000000..dbf5bd3 --- /dev/null +++ b/test/telegram/handlers/myGroups_test.go @@ -0,0 +1,86 @@ +package test + +import ( + "algobot/internal/domain/models" + "algobot/internal/telegram/handlers/text" + "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" + "time" +) + +func TestMyGropus(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + grouper := mocks2.NewMockGrouper(ctrl) + ser := mocks2.NewMockGroupSerializer(ctrl) + botName := "name" + mctx := mocks3.NewMockContext(ctrl) + + handler := text.NewMyGroup(log, grouper, ser, botName) + + mctx.EXPECT().Get(gomock.Any()).Return("trace_id").AnyTimes() + t.Run("happyPath", func(t *testing.T) { + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + grouper.EXPECT().Groups(int64(1)).Return(mockGroups, nil).Times(1), + ser.EXPECT().Serialize(mockGroups[0]).Return("ser-g1", nil).Times(1), + ser.EXPECT().Serialize(mockGroups[1]).Return("ser-g2", nil).Times(1), + ser.EXPECT().Serialize(mockGroups[2]).Return("", errors.New("ser")).Times(1), + mctx.EXPECT().Send(mockStringRet, tele.ModeHTML).Return(nil).Times(1), + ) + + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("no one group", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + grouper.EXPECT().Groups(int64(1)).Return([]models.Group{}, nil).Times(1), + mctx.EXPECT().Send("Всего групп: 0\nПопробуйте обновить группы!", tele.ModeHTML).Return(nil).Times(1), + ) + + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("Groups return err", func(t *testing.T) { + err := errors.New("groups err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + grouper.EXPECT().Groups(int64(1)).Return(nil, err).Times(1), + mctx.EXPECT().Send("[trace_id] Ошибка при получении групп!", tele.ModeHTML).Return(nil).Times(1), + ) + + err = handler.ServeContext(mctx) + assert.NoError(t, err) + }) +} + +var mockStringRet = "Всего групп: 3\n\n1. [Name 1](t.me/name?start=ser-g1) 🕐 вт 12:00\n2. [Name 2](t.me/name?start=ser-g2) 🕐 вт 13:00\n\n1. Name 3 🕐 ср 12:00" + +var mockGroups = []models.Group{ + { + GroupID: 1, + Title: "Name 1", + TimeLesson: time.Date(2020, time.April, 14, 12, 0, 0, 0, time.UTC), + }, + { + GroupID: 2, + Title: "Name 2", + TimeLesson: time.Date(2020, time.April, 14, 13, 0, 0, 0, time.UTC), + }, + { + GroupID: 3, + Title: "Name 3", + TimeLesson: time.Date(2020, time.April, 15, 12, 0, 0, 0, time.UTC), + }, +} From 63c2436ffd814f9ce79fab942820599e0b445ff6 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 11:40:08 +0300 Subject: [PATCH 16/44] add handler myGroups + test --- go.mod | 1 + go.sum | 2 + internal/app/app.go | 2 +- internal/app/telegram/app.go | 10 ++- .../telegram/keyboards/refreshGroups.go | 14 ++++ internal/lib/serdes/base62/base62.go | 26 +++++++ internal/lib/serdes/group.go | 8 +++ internal/lib/sort/groups.go | 17 +++++ internal/services/group.go | 40 +++++++++++ internal/storage/sqlite/groups.go | 49 +++++++++++++ internal/telegram/handlers/text/myGroups.go | 11 +-- test/lib/sort/group_test.go | 49 +++++++++++++ test/mocks/services/mockgen.go | 3 + test/services/group_test.go | 72 +++++++++++++++++++ .../03_append_user_in_users_table.sql | 2 +- .../04_append_groups_in_groups_table.sql | 11 +++ test/storage/sqlite_test.go | 31 ++++++++ test/telegram/handlers/myGroups_test.go | 19 ++--- 18 files changed, 349 insertions(+), 18 deletions(-) create mode 100644 internal/domain/telegram/keyboards/refreshGroups.go create mode 100644 internal/lib/serdes/base62/base62.go create mode 100644 internal/lib/serdes/group.go create mode 100644 internal/lib/sort/groups.go create mode 100644 internal/services/group.go create mode 100644 internal/storage/sqlite/groups.go create mode 100644 test/lib/sort/group_test.go create mode 100644 test/mocks/services/mockgen.go create mode 100644 test/services/group_test.go create mode 100644 test/storage/migrations-suite/04_append_groups_in_groups_table.sql diff --git a/go.mod b/go.mod index 4404c3b..a0eb9b6 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/LZTD1/telebot-context-router v1.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/jxskiss/base62 v1.1.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 777dfda..a9d246a 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= +github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/internal/app/app.go b/internal/app/app.go index 213f4f6..1073f14 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,7 +19,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } - botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage, storage) + botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage, storage, storage) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 3a7ed30..ca5de57 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -4,6 +4,8 @@ import ( "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" + "algobot/internal/lib/serdes/base62" + "algobot/internal/services" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" @@ -25,7 +27,7 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter, notifChanger callback.NotificationChanger) *App { +func New(log *slog.Logger, token string, grGetter services.GroupGetter, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter, notifChanger callback.NotificationChanger) *App { const op = "telegram.New" nlog := log.With( @@ -49,7 +51,10 @@ func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInform os.Exit(1) } + // dependencies + groupServ := services.NewGroup(log, grGetter) stateMachine := memory.New() + serdes := base62.NewSerdes(log) // initialize routes b.Use(trace.New(log)) @@ -58,14 +63,15 @@ func New(log *slog.Logger, token string, auther auth.Auther, set text.UserInform b.Use(logger.New(log)) b.Use(auth.New(auther, log)) + // create routing r := router.NewRouter() - //st := stater.New(stateMachine, fsm.Default) r.Group(func(r router.Router) { // Routes for default state r.Use(stater.New(stateMachine, fsm.Default)) // message r.HandleFuncText("/start", text.NewStart(stateMachine)) r.HandleFuncText("Настройки", text.NewSettings(set, log)) + r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) // callbacks r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) diff --git a/internal/domain/telegram/keyboards/refreshGroups.go b/internal/domain/telegram/keyboards/refreshGroups.go new file mode 100644 index 0000000..2e7e674 --- /dev/null +++ b/internal/domain/telegram/keyboards/refreshGroups.go @@ -0,0 +1,14 @@ +package keyboards + +import tele "gopkg.in/telebot.v4" + +func RefreshGroups() *tele.ReplyMarkup { + refreshKb := &tele.ReplyMarkup{ResizeKeyboard: true} + refresh := refreshKb.Data("Обновить группы", "refresh_groups") + + refreshKb.Inline( + refreshKb.Row(refresh), + ) + + return refreshKb +} diff --git a/internal/lib/serdes/base62/base62.go b/internal/lib/serdes/base62/base62.go new file mode 100644 index 0000000..4d8964f --- /dev/null +++ b/internal/lib/serdes/base62/base62.go @@ -0,0 +1,26 @@ +package base62 + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/serdes" + "fmt" + "github.com/jxskiss/base62" + "log/slog" +) + +type Serdes struct { + log *slog.Logger +} + +func NewSerdes(log *slog.Logger) *Serdes { + return &Serdes{log: log} +} + +func (s *Serdes) Serialize(group models.Group, traceID interface{}) (string, error) { + encoded := base62.EncodeToString([]byte(fmt.Sprintf( + "%d-%d", + serdes.GroupType, + group.GroupID, + ))) + return encoded, nil +} diff --git a/internal/lib/serdes/group.go b/internal/lib/serdes/group.go new file mode 100644 index 0000000..26fe357 --- /dev/null +++ b/internal/lib/serdes/group.go @@ -0,0 +1,8 @@ +package serdes + +type SerType int + +const ( + GroupType SerType = iota + UserType +) diff --git a/internal/lib/sort/groups.go b/internal/lib/sort/groups.go new file mode 100644 index 0000000..c40eab2 --- /dev/null +++ b/internal/lib/sort/groups.go @@ -0,0 +1,17 @@ +package sort + +import ( + "algobot/internal/domain/models" + "sort" +) + +func GroupsByDate(groups []models.Group) { + sort.Slice(groups, func(i, j int) bool { + dayI, dayJ := groups[i].TimeLesson.Weekday(), groups[j].TimeLesson.Weekday() + if dayI != dayJ { + return dayI > dayJ + } + return groups[i].TimeLesson.Hour() < groups[j].TimeLesson.Hour() || + (groups[i].TimeLesson.Hour() == groups[j].TimeLesson.Hour() && groups[i].TimeLesson.Minute() < groups[j].TimeLesson.Minute()) + }) +} diff --git a/internal/services/group.go b/internal/services/group.go new file mode 100644 index 0000000..fd67331 --- /dev/null +++ b/internal/services/group.go @@ -0,0 +1,40 @@ +package services + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "algobot/internal/lib/sort" + "fmt" + "log/slog" +) + +type GroupGetter interface { + Groups(uid int64) ([]models.Group, error) +} + +type Group struct { + log *slog.Logger + getter GroupGetter +} + +func NewGroup(log *slog.Logger, getter GroupGetter) *Group { + return &Group{log: log, getter: getter} +} + +func (g *Group) Groups(uid int64, traceID interface{}) ([]models.Group, error) { + const op = "services.Group.Groups" + log := g.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + groups, err := g.getter.Groups(uid) + if err != nil { + log.Warn("error while get groups", sl.Err(err)) + return []models.Group{}, fmt.Errorf("%s error while get groups: %w", op, err) + } + + sort.GroupsByDate(groups) + + return groups, nil +} diff --git a/internal/storage/sqlite/groups.go b/internal/storage/sqlite/groups.go new file mode 100644 index 0000000..0c4887f --- /dev/null +++ b/internal/storage/sqlite/groups.go @@ -0,0 +1,49 @@ +package sqlite + +import ( + "algobot/internal/domain/models" + "database/sql" + "fmt" + "time" +) + +func (s *Sqlite) Groups(uid int64) ([]models.Group, error) { + const op = "sqlite.Groups" + + sqlq := "SELECT group_id, title, time_lesson FROM groups WHERE owner_id=?" + pr, err := s.db.Prepare(sqlq) + if err != nil { + return nil, fmt.Errorf("%s error while preparing sql: %w", op, err) + } + defer pr.Close() + + rows, err := pr.Query(uid) + if err != nil { + return nil, fmt.Errorf("%s error while executing sql: %w", op, err) + } + defer rows.Close() + + var groups []models.Group + for rows.Next() { + var groupId sql.NullInt64 + var title sql.NullString + var timeGroup sql.NullString + + if err := rows.Scan(&groupId, &title, &timeGroup); err != nil { + return nil, fmt.Errorf("%s error while scanning row: %w", op, err) + } + + parsedTime, err := time.Parse("2006-01-02 15:04:05", timeGroup.String) + if err != nil { + return nil, fmt.Errorf("%s error while parsing time: %w", op, err) + } + + groups = append(groups, models.Group{ + GroupID: int(groupId.Int64), + Title: title.String, + TimeLesson: parsedTime, + }) + } + + return groups, nil +} diff --git a/internal/telegram/handlers/text/myGroups.go b/internal/telegram/handlers/text/myGroups.go index f791a1c..5cf93ca 100644 --- a/internal/telegram/handlers/text/myGroups.go +++ b/internal/telegram/handlers/text/myGroups.go @@ -2,6 +2,7 @@ package text import ( "algobot/internal/domain/models" + "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/logger/sl" "fmt" "gopkg.in/telebot.v4" @@ -21,11 +22,11 @@ var locales = map[time.Weekday]string{ } type Grouper interface { - Groups(uid int64) ([]models.Group, error) + Groups(uid int64, traceID interface{}) ([]models.Group, error) } type GroupSerializer interface { - Serialize(group models.Group) (string, error) + Serialize(group models.Group, traceID interface{}) (string, error) } type MyGroup struct { log *slog.Logger @@ -51,13 +52,13 @@ func (g *MyGroup) ServeContext(ctx telebot.Context) error { ) uid := ctx.Sender().ID - groups, err := g.grouper.Groups(uid) + groups, err := g.grouper.Groups(uid, ctx.Get("trace_id")) if err != nil { log.Warn("error while getting groups", sl.Err(err)) return ctx.Send(fmt.Sprintf("[%s] Ошибка при получении групп!", ctx.Get("trace_id")), telebot.ModeHTML) } - return ctx.Send(g.msgMyGroups(groups, ctx), telebot.ModeHTML) + return ctx.Send(g.msgMyGroups(groups, ctx), telebot.ModeHTML, keyboards.RefreshGroups()) } func (g *MyGroup) msgMyGroups(groups []models.Group, ctx telebot.Context) string { @@ -98,7 +99,7 @@ func (g *MyGroup) getFormattedTitle(group models.Group, ctx telebot.Context) str slog.Any("trace_id", ctx.Get("trace_id")), ) - serialized, err := g.serializer.Serialize(group) + serialized, err := g.serializer.Serialize(group, ctx.Get("trace_id")) if err != nil { log.Warn("error while serializing group", sl.Err(err)) return group.Title diff --git a/test/lib/sort/group_test.go b/test/lib/sort/group_test.go new file mode 100644 index 0000000..d6563a7 --- /dev/null +++ b/test/lib/sort/group_test.go @@ -0,0 +1,49 @@ +package sort + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/sort" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSortGroup(t *testing.T) { + sort.GroupsByDate(assets) + assert.Equal(t, []models.Group{ + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + }, assets) +} + +var assets = []models.Group{ + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, +} diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go new file mode 100644 index 0000000..9760188 --- /dev/null +++ b/test/mocks/services/mockgen.go @@ -0,0 +1,3 @@ +package mocks + +//go:generate mockgen -destination=./groupGetter_mock.go -package=mocks algobot/internal/services GroupGetter diff --git a/test/services/group_test.go b/test/services/group_test.go new file mode 100644 index 0000000..a98eed0 --- /dev/null +++ b/test/services/group_test.go @@ -0,0 +1,72 @@ +package test + +import ( + "algobot/internal/domain/models" + "algobot/internal/services" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/services" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func TestGroup(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + gGetter := mocks2.NewMockGroupGetter(ctrl) + + service := services.NewGroup(log, gGetter) + + t.Run("happy path", func(t *testing.T) { + gGetter.EXPECT().Groups(int64(1)).Return(assets, nil).Times(1) + gr, err := service.Groups(1, "trace_id") + assert.NoError(t, err) + assert.Equal(t, []models.Group{ + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + }, gr) + }) + t.Run("Groups return err", func(t *testing.T) { + errExp := errors.New("some error") + gGetter.EXPECT().Groups(int64(1)).Return(nil, errExp).Times(1) + _, err := service.Groups(1, "trace_id") + assert.ErrorIs(t, err, errExp) + }) + +} + +var assets = []models.Group{ + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, +} diff --git a/test/storage/migrations-suite/03_append_user_in_users_table.sql b/test/storage/migrations-suite/03_append_user_in_users_table.sql index 5880277..0d5fe0f 100644 --- a/test/storage/migrations-suite/03_append_user_in_users_table.sql +++ b/test/storage/migrations-suite/03_append_user_in_users_table.sql @@ -8,4 +8,4 @@ VALUES (1001, 'cookie', NULL, 0), -- +goose Down DELETE FROM users -WHERE uid in (1001, 1002) \ No newline at end of file +WHERE uid in (1001, 1000, 999) \ No newline at end of file diff --git a/test/storage/migrations-suite/04_append_groups_in_groups_table.sql b/test/storage/migrations-suite/04_append_groups_in_groups_table.sql new file mode 100644 index 0000000..d53d281 --- /dev/null +++ b/test/storage/migrations-suite/04_append_groups_in_groups_table.sql @@ -0,0 +1,11 @@ +-- +goose Up +INSERT INTO groups (group_id, owner_id, title, time_lesson) +VALUES (1001, 999, 'group 1', '2025-03-23 14:00:00'), + (1000, 999, 'group 2', '2025-03-23 16:00:00'), + (999, 999, 'group 3', '2025-03-22 14:00:00'); + + +-- +goose Down +DELETE +FROM groups +WHERE group_id in (1001, 1000, 999) \ No newline at end of file diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index 4554048..6d1016f 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -2,6 +2,7 @@ package test import ( "algobot/internal/config" + "algobot/internal/domain/models" sqlite2 "algobot/internal/storage/sqlite" "database/sql" "fmt" @@ -11,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "os" "testing" + "time" ) const ( @@ -115,4 +117,33 @@ func TestSqlite(t *testing.T) { assert.NoError(t, err) assert.False(t, notif) }) + t.Run("Groups", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + groups, err := sqlite.Groups(999) + assert.NoError(t, err) + assert.Len(t, groups, 3) + assert.Equal(t, []models.Group{ + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, + }, groups) + }) + t.Run("no one group", func(t *testing.T) { + groups, err := sqlite.Groups(1000) + assert.NoError(t, err) + assert.Len(t, groups, 0) + }) + }) } diff --git a/test/telegram/handlers/myGroups_test.go b/test/telegram/handlers/myGroups_test.go index dbf5bd3..c9f0cf6 100644 --- a/test/telegram/handlers/myGroups_test.go +++ b/test/telegram/handlers/myGroups_test.go @@ -2,6 +2,7 @@ package test import ( "algobot/internal/domain/models" + "algobot/internal/domain/telegram/keyboards" "algobot/internal/telegram/handlers/text" "algobot/test/mocks" mocks3 "algobot/test/mocks/telegram" @@ -14,7 +15,7 @@ import ( "time" ) -func TestMyGropus(t *testing.T) { +func TestMyGroups(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -31,11 +32,11 @@ func TestMyGropus(t *testing.T) { gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), - grouper.EXPECT().Groups(int64(1)).Return(mockGroups, nil).Times(1), - ser.EXPECT().Serialize(mockGroups[0]).Return("ser-g1", nil).Times(1), - ser.EXPECT().Serialize(mockGroups[1]).Return("ser-g2", nil).Times(1), - ser.EXPECT().Serialize(mockGroups[2]).Return("", errors.New("ser")).Times(1), - mctx.EXPECT().Send(mockStringRet, tele.ModeHTML).Return(nil).Times(1), + grouper.EXPECT().Groups(int64(1), "trace_id").Return(mockGroups, nil).Times(1), + ser.EXPECT().Serialize(mockGroups[0], "trace_id").Return("ser-g1", nil).Times(1), + ser.EXPECT().Serialize(mockGroups[1], "trace_id").Return("ser-g2", nil).Times(1), + ser.EXPECT().Serialize(mockGroups[2], "trace_id").Return("", errors.New("ser")).Times(1), + mctx.EXPECT().Send(mockStringRet, tele.ModeHTML, keyboards.RefreshGroups()).Return(nil).Times(1), ) err := handler.ServeContext(mctx) @@ -44,8 +45,8 @@ func TestMyGropus(t *testing.T) { t.Run("no one group", func(t *testing.T) { gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), - grouper.EXPECT().Groups(int64(1)).Return([]models.Group{}, nil).Times(1), - mctx.EXPECT().Send("Всего групп: 0\nПопробуйте обновить группы!", tele.ModeHTML).Return(nil).Times(1), + grouper.EXPECT().Groups(int64(1), "trace_id").Return([]models.Group{}, nil).Times(1), + mctx.EXPECT().Send("Всего групп: 0\nПопробуйте обновить группы!", tele.ModeHTML, keyboards.RefreshGroups()).Return(nil).Times(1), ) err := handler.ServeContext(mctx) @@ -56,7 +57,7 @@ func TestMyGropus(t *testing.T) { gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), - grouper.EXPECT().Groups(int64(1)).Return(nil, err).Times(1), + grouper.EXPECT().Groups(int64(1), "trace_id").Return(nil, err).Times(1), mctx.EXPECT().Send("[trace_id] Ошибка при получении групп!", tele.ModeHTML).Return(nil).Times(1), ) From d55e71a8a89a24b217261e38d8d2f65aa8d0eb86 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 14:02:47 +0300 Subject: [PATCH 17/44] add rate limiter + tests --- config/dev.yaml | 3 ++ go.mod | 5 ++- go.sum | 3 +- internal/app/app.go | 11 +++++- internal/app/telegram/app.go | 14 ++++++- internal/config/config.go | 17 +++++--- internal/telegram/middleware/rate/rate.go | 47 +++++++++++++++++++++++ test/telegram/middlewares/rate_test.go | 42 ++++++++++++++++++++ 8 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 internal/telegram/middleware/rate/rate.go create mode 100644 test/telegram/middlewares/rate_test.go diff --git a/config/dev.yaml b/config/dev.yaml index 59a5ceb..d0374cf 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -6,3 +6,6 @@ grpc: host: "localhost" port: 50051 timeout: 999s +rate_limit: + fill_period: 800ms + bucket_limit: 6 \ No newline at end of file diff --git a/go.mod b/go.mod index a0eb9b6..79d062f 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,22 @@ module algobot go 1.23.3 require ( + github.com/LZTD1/telebot-context-router v1.0.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/jxskiss/base62 v1.1.0 github.com/ncruces/go-sqlite3 v0.25.0 github.com/pressly/goose/v3 v3.24.2 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 gopkg.in/telebot.v4 v4.0.0-beta.4 ) require ( github.com/BurntSushi/toml v1.5.0 // indirect - github.com/LZTD1/telebot-context-router v1.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect - github.com/jxskiss/base62 v1.1.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index a9d246a..4892359 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/LZTD1/telebot-context-router v1.0.1 h1:iGrKhFevnXFtQre+3bum6g9AluMTE23IVz2A+cOZPYE= github.com/LZTD1/telebot-context-router v1.0.1/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= -github.com/LZTD1/telebot-router v1.1.1 h1:YNIs70tnMSjpZNFQxYhS6GcUfPBugR5FSUlJ1S3mvrc= -github.com/LZTD1/telebot-router v1.1.1/go.mod h1:k9h+Glmg+h36Wguq7+ycs7saP1cu3tdANde5pRWI2/A= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -643,6 +641,7 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/app/app.go b/internal/app/app.go index 1073f14..e94559a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,7 +19,16 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } - botApplication := telegram.New(log, cfg.TelegramToken, storage, storage, storage, storage, storage) + botApplication := telegram.New( + log, + cfg.TelegramToken, + storage, + storage, + storage, + storage, + storage, + cfg.RateLimit, + ) return &App{log: log, cfg: cfg, TelegramBot: botApplication} } diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index ca5de57..c6d7aa5 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -1,6 +1,7 @@ package telegram import ( + "algobot/internal/config" "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" @@ -10,6 +11,7 @@ import ( "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" "algobot/internal/telegram/middleware/logger" + "algobot/internal/telegram/middleware/rate" "algobot/internal/telegram/middleware/stater" "algobot/internal/telegram/middleware/trace" "fmt" @@ -27,7 +29,16 @@ type App struct { bot *tele.Bot } -func New(log *slog.Logger, token string, grGetter services.GroupGetter, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter, notifChanger callback.NotificationChanger) *App { +func New( + log *slog.Logger, + token string, + grGetter services.GroupGetter, + auther auth.Auther, + set text.UserInformer, + cookieSetter text.CookieSetter, + notifChanger callback.NotificationChanger, + rateCfg config.RateLimit, +) *App { const op = "telegram.New" nlog := log.With( @@ -62,6 +73,7 @@ func New(log *slog.Logger, token string, grGetter services.GroupGetter, auther a b.Use(middleware.Recover()) b.Use(logger.New(log)) b.Use(auth.New(auther, log)) + b.Use(rate.New(log, rateCfg)) // create routing r := router.NewRouter() diff --git a/internal/config/config.go b/internal/config/config.go index 9e8d051..0841e0e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,14 +4,21 @@ import ( "flag" "github.com/ilyakaznacheev/cleanenv" "os" + "time" ) type Config struct { - Env string `yaml:"env" env-default:"prod"` - TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` - MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` - StoragePath string `yaml:"storage_path" env-default:"./storage/storage.db"` - GRPC GRPC `yaml:"grpc"` + Env string `yaml:"env" env-default:"prod"` + TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` + MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` + StoragePath string `yaml:"storage_path" env-default:"./storage/storage.db"` + GRPC GRPC `yaml:"grpc"` + RateLimit RateLimit `yaml:"rate_limit"` +} + +type RateLimit struct { + FillPeriod time.Duration `yaml:"fill_period" env-default:"1s"` + BucketLimit int `yaml:"bucket_limit" env-default:"10"` } type GRPC struct { diff --git a/internal/telegram/middleware/rate/rate.go b/internal/telegram/middleware/rate/rate.go new file mode 100644 index 0000000..189a359 --- /dev/null +++ b/internal/telegram/middleware/rate/rate.go @@ -0,0 +1,47 @@ +package rate + +import ( + "algobot/internal/config" + "golang.org/x/time/rate" + "gopkg.in/telebot.v4" + "log/slog" + "sync" +) + +var userLimits = make(map[int64]*rate.Limiter) +var mu sync.Mutex + +func New(log *slog.Logger, rateCfg config.RateLimit) telebot.MiddlewareFunc { + return func(next telebot.HandlerFunc) telebot.HandlerFunc { + log = log.With( + slog.String("component", "middleware/rate"), + ) + + return func(ctx telebot.Context) error { + traceID := ctx.Get("trace_id") + log = log.With("trace_id", traceID) + uid := ctx.Sender().ID + + limiter := getUserLimiter(uid, rateCfg) + if !limiter.Allow() { + log.Warn("user limit exceeded") + return ctx.Send("🙈 Слишком много запросов, давай помедленнее!") + } + + return next(ctx) + } + } +} + +func getUserLimiter(uid int64, cfg config.RateLimit) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + if limiter, ok := userLimits[uid]; ok { + return limiter + } + + limiter := rate.NewLimiter(rate.Every(cfg.FillPeriod), cfg.BucketLimit) + userLimits[uid] = limiter + return limiter +} diff --git a/test/telegram/middlewares/rate_test.go b/test/telegram/middlewares/rate_test.go new file mode 100644 index 0000000..6711498 --- /dev/null +++ b/test/telegram/middlewares/rate_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "algobot/internal/config" + rate2 "algobot/internal/telegram/middleware/rate" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/telegram" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" + "time" +) + +func TestRate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + expected := 3 + + actual := 0 + + rate := rate2.New(log, config.RateLimit{ + FillPeriod: 1 * time.Second, + BucketLimit: expected, + }) + + mctx := mocks2.NewMockContext(ctrl) + mctx.EXPECT().Get(gomock.Any()).Return(nil).AnyTimes() + mctx.EXPECT().Sender().Return(&telebot.User{ID: 1}).AnyTimes() + + mctx.EXPECT().Send(gomock.Any()).Return(nil).AnyTimes() + hfunc := func(ctx telebot.Context) error { + actual++ + return nil + } + for i := 0; i < expected+1; i++ { + rate(hfunc)(mctx) + } + assert.Equal(t, expected, actual) +} From 12d2db7412c2c34e6b349210b6915086b994610e Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 14:04:03 +0300 Subject: [PATCH 18/44] add rate limiter + tests --- test/telegram/middlewares/rate_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/telegram/middlewares/rate_test.go b/test/telegram/middlewares/rate_test.go index 6711498..1f2fa3b 100644 --- a/test/telegram/middlewares/rate_test.go +++ b/test/telegram/middlewares/rate_test.go @@ -36,7 +36,8 @@ func TestRate(t *testing.T) { return nil } for i := 0; i < expected+1; i++ { - rate(hfunc)(mctx) + err := rate(hfunc)(mctx) + assert.NoError(t, err) } assert.Equal(t, expected, actual) } From 0d67d72bbac05638636a46501e456e17e35cfdfd Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 4 Apr 2025 16:11:01 +0300 Subject: [PATCH 19/44] add grpc service --- Makefile | 8 + config/dev.yaml | 2 +- go.mod | 5 + go.sum | 9 + internal/app/app.go | 1 + internal/app/telegram/app.go | 5 + internal/config/config.go | 6 +- internal/domain/models/aiinfo.go | 6 + internal/services/grpc/ai.go | 55 ++ internal/telegram/handlers/text/ai.go | 12 +- protos/ai.pb.go | 693 ++++++++++++++++++++++++++ protos/ai.proto | 58 +++ protos/ai_grpc.pb.go | 273 ++++++++++ test/telegram/handlers/ai_test.go | 9 +- 14 files changed, 1126 insertions(+), 16 deletions(-) create mode 100644 internal/domain/models/aiinfo.go create mode 100644 internal/services/grpc/ai.go create mode 100644 protos/ai.pb.go create mode 100644 protos/ai.proto create mode 100644 protos/ai_grpc.pb.go diff --git a/Makefile b/Makefile index 76f680b..c63267b 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,14 @@ dev: mock-gen: cd test && go generate ./... +.PHONY: grpc-gen +grpc-gen: + protoc --go_out=. \ + --go_opt=paths=source_relative \ + --go-grpc_out=. \ + --go-grpc_opt=paths=source_relative \ + ./protos/*.proto + .PHONY: migrate migrate: go run ./cmd/migrator/main.go -migrations-path=./migrations -storage-path=./storage/storage.db \ No newline at end of file diff --git a/config/dev.yaml b/config/dev.yaml index d0374cf..b80c9e8 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -5,7 +5,7 @@ migrations_path: "./migrations" grpc: host: "localhost" port: 50051 - timeout: 999s + timeout: 300s rate_limit: fill_period: 800ms bucket_limit: 6 \ No newline at end of file diff --git a/go.mod b/go.mod index 79d062f..f730b06 100644 --- a/go.mod +++ b/go.mod @@ -25,8 +25,13 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc v1.71.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 4892359..b38d73c 100644 --- a/go.sum +++ b/go.sum @@ -512,6 +512,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -827,7 +829,10 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -858,6 +863,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -873,6 +880,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/app/app.go b/internal/app/app.go index e94559a..4deb068 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -28,6 +28,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { storage, storage, cfg.RateLimit, + cfg.GRPC, ) return &App{log: log, cfg: cfg, TelegramBot: botApplication} diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index c6d7aa5..2ca7255 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -7,6 +7,7 @@ import ( "algobot/internal/lib/logger/sl" "algobot/internal/lib/serdes/base62" "algobot/internal/services" + grpc2 "algobot/internal/services/grpc" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" @@ -38,6 +39,8 @@ func New( cookieSetter text.CookieSetter, notifChanger callback.NotificationChanger, rateCfg config.RateLimit, + grpcCfg config.GRPC, + ) *App { const op = "telegram.New" @@ -66,6 +69,7 @@ func New( groupServ := services.NewGroup(log, grGetter) stateMachine := memory.New() serdes := base62.NewSerdes(log) + grpc := grpc2.NewAIService(grpcCfg, log) // initialize routes b.Use(trace.New(log)) @@ -83,6 +87,7 @@ func New( // message r.HandleFuncText("/start", text.NewStart(stateMachine)) r.HandleFuncText("Настройки", text.NewSettings(set, log)) + r.HandleFuncText("AI 🔹", text.NewAI(grpc, log, stateMachine)) r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) // callbacks diff --git a/internal/config/config.go b/internal/config/config.go index 0841e0e..e7c0d40 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,9 +22,9 @@ type RateLimit struct { } type GRPC struct { - Host string `yaml:"host" env-default:"localhost"` - Port string `yaml:"port" env-default:"50051"` - Timeout string `yaml:"timeout" env-default:"600s"` + Host string `yaml:"host" env-default:"localhost"` + Port string `yaml:"port" env-default:"50051"` + Timeout time.Duration `yaml:"timeout" env-default:"600s"` } func MustLoad() *Config { diff --git a/internal/domain/models/aiinfo.go b/internal/domain/models/aiinfo.go new file mode 100644 index 0000000..9c95769 --- /dev/null +++ b/internal/domain/models/aiinfo.go @@ -0,0 +1,6 @@ +package models + +type AIInfo struct { + TextModel string + ImageModel string +} diff --git a/internal/services/grpc/ai.go b/internal/services/grpc/ai.go new file mode 100644 index 0000000..3da91ec --- /dev/null +++ b/internal/services/grpc/ai.go @@ -0,0 +1,55 @@ +package grpc + +import ( + "algobot/internal/config" + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + aiv1 "algobot/protos" + "context" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "log/slog" + "time" +) + +type AIService struct { + grpc aiv1.AiClient + timeout time.Duration + log *slog.Logger +} + +func NewAIService(grpcCfg config.GRPC, log *slog.Logger) *AIService { + conn, err := grpc.NewClient(fmt.Sprintf("%s:%s", grpcCfg.Host, grpcCfg.Port), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + return &AIService{ + grpc: aiv1.NewAiClient(conn), + timeout: grpcCfg.Timeout, + log: log, + } +} + +func (a *AIService) GetAIInfo(traceID interface{}) (models.AIInfo, error) { + const op = "grpc.AIService.GetAIInfo" + log := a.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + ctx, cancel := context.WithTimeout(context.Background(), a.timeout) + defer cancel() + + information, err := a.grpc.GetInformation(ctx, &aiv1.GetInformationRequest{}) + if err != nil { + log.Warn("error while calling gRPC GetInformation", sl.Err(err)) + return models.AIInfo{}, fmt.Errorf("%s error while calling gRPC GetInformation: %w", op, err) + } + + return models.AIInfo{ + TextModel: information.GetChatModel(), + ImageModel: information.GetImageModel(), + }, nil +} diff --git a/internal/telegram/handlers/text/ai.go b/internal/telegram/handlers/text/ai.go index 6c966de..dfe6e28 100644 --- a/internal/telegram/handlers/text/ai.go +++ b/internal/telegram/handlers/text/ai.go @@ -1,6 +1,7 @@ package text import ( + "algobot/internal/domain/models" "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/fsm" "algobot/internal/lib/logger/sl" @@ -9,13 +10,8 @@ import ( "strings" ) -type AIInfo struct { - TextModel string - ImageModel string -} - type AIInformer interface { - GetAIInfo() (AIInfo, error) + GetAIInfo(traceID interface{}) (models.AIInfo, error) } type AIStater interface { @@ -32,7 +28,7 @@ func NewAI(ai AIInformer, log *slog.Logger, stater AIStater) telebot.HandlerFunc ) uid := ctx.Sender().ID - info, err := ai.GetAIInfo() + info, err := ai.GetAIInfo(ctx.Get("trace_id")) if err != nil { log.Warn("error while GetAIInfo", sl.Err(err)) return ctx.Send("Упс, AI сейчас не работает!") @@ -43,7 +39,7 @@ func NewAI(ai AIInformer, log *slog.Logger, stater AIStater) telebot.HandlerFunc } } -func GetAIMessage(info AIInfo) string { +func GetAIMessage(info models.AIInfo) string { sb := strings.Builder{} sb.WriteString("Информация о включенных моделях:\n\n") sb.WriteString("Текст: ") diff --git a/protos/ai.pb.go b/protos/ai.pb.go new file mode 100644 index 0000000..d495c8e --- /dev/null +++ b/protos/ai.pb.go @@ -0,0 +1,693 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v5.29.3 +// source: protos/ai.proto + +package aiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ModelType int32 + +const ( + ModelType_MODEL_UNSPECIFIED ModelType = 0 + ModelType_IMAGE_MODEL ModelType = 1 + ModelType_TEXT_MODEL ModelType = 2 +) + +// Enum value maps for ModelType. +var ( + ModelType_name = map[int32]string{ + 0: "MODEL_UNSPECIFIED", + 1: "IMAGE_MODEL", + 2: "TEXT_MODEL", + } + ModelType_value = map[string]int32{ + "MODEL_UNSPECIFIED": 0, + "IMAGE_MODEL": 1, + "TEXT_MODEL": 2, + } +) + +func (x ModelType) Enum() *ModelType { + p := new(ModelType) + *p = x + return p +} + +func (x ModelType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ModelType) Descriptor() protoreflect.EnumDescriptor { + return file_protos_ai_proto_enumTypes[0].Descriptor() +} + +func (ModelType) Type() protoreflect.EnumType { + return &file_protos_ai_proto_enumTypes[0] +} + +func (x ModelType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ModelType.Descriptor instead. +func (ModelType) EnumDescriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{0} +} + +type GenerateImageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` + Promt string `protobuf:"bytes,2,opt,name=promt,proto3" json:"promt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateImageRequest) Reset() { + *x = GenerateImageRequest{} + mi := &file_protos_ai_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateImageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateImageRequest) ProtoMessage() {} + +func (x *GenerateImageRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateImageRequest.ProtoReflect.Descriptor instead. +func (*GenerateImageRequest) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{0} +} + +func (x *GenerateImageRequest) GetUid() int64 { + if x != nil { + return x.Uid + } + return 0 +} + +func (x *GenerateImageRequest) GetPromt() string { + if x != nil { + return x.Promt + } + return "" +} + +type GenerateImageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateImageResponse) Reset() { + *x = GenerateImageResponse{} + mi := &file_protos_ai_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateImageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateImageResponse) ProtoMessage() {} + +func (x *GenerateImageResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateImageResponse.ProtoReflect.Descriptor instead. +func (*GenerateImageResponse) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{1} +} + +func (x *GenerateImageResponse) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type ChangeModelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type ModelType `protobuf:"varint,1,opt,name=type,proto3,enum=pkg.ModelType" json:"type,omitempty"` + ModelName string `protobuf:"bytes,2,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeModelRequest) Reset() { + *x = ChangeModelRequest{} + mi := &file_protos_ai_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeModelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeModelRequest) ProtoMessage() {} + +func (x *ChangeModelRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeModelRequest.ProtoReflect.Descriptor instead. +func (*ChangeModelRequest) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{2} +} + +func (x *ChangeModelRequest) GetType() ModelType { + if x != nil { + return x.Type + } + return ModelType_MODEL_UNSPECIFIED +} + +func (x *ChangeModelRequest) GetModelName() string { + if x != nil { + return x.ModelName + } + return "" +} + +type ChangeModelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeModelResponse) Reset() { + *x = ChangeModelResponse{} + mi := &file_protos_ai_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeModelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeModelResponse) ProtoMessage() {} + +func (x *ChangeModelResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeModelResponse.ProtoReflect.Descriptor instead. +func (*ChangeModelResponse) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{3} +} + +func (x *ChangeModelResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *ChangeModelResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type GetInformationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInformationRequest) Reset() { + *x = GetInformationRequest{} + mi := &file_protos_ai_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInformationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInformationRequest) ProtoMessage() {} + +func (x *GetInformationRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInformationRequest.ProtoReflect.Descriptor instead. +func (*GetInformationRequest) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{4} +} + +type GetInformationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChatModel string `protobuf:"bytes,1,opt,name=chat_model,json=chatModel,proto3" json:"chat_model,omitempty"` + ImageModel string `protobuf:"bytes,2,opt,name=image_model,json=imageModel,proto3" json:"image_model,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInformationResponse) Reset() { + *x = GetInformationResponse{} + mi := &file_protos_ai_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInformationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInformationResponse) ProtoMessage() {} + +func (x *GetInformationResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInformationResponse.ProtoReflect.Descriptor instead. +func (*GetInformationResponse) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{5} +} + +func (x *GetInformationResponse) GetChatModel() string { + if x != nil { + return x.ChatModel + } + return "" +} + +func (x *GetInformationResponse) GetImageModel() string { + if x != nil { + return x.ImageModel + } + return "" +} + +type SuggestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` + Suggest string `protobuf:"bytes,2,opt,name=suggest,proto3" json:"suggest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SuggestRequest) Reset() { + *x = SuggestRequest{} + mi := &file_protos_ai_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SuggestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SuggestRequest) ProtoMessage() {} + +func (x *SuggestRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SuggestRequest.ProtoReflect.Descriptor instead. +func (*SuggestRequest) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{6} +} + +func (x *SuggestRequest) GetUid() int64 { + if x != nil { + return x.Uid + } + return 0 +} + +func (x *SuggestRequest) GetSuggest() string { + if x != nil { + return x.Suggest + } + return "" +} + +type SuggestResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Request string `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SuggestResponse) Reset() { + *x = SuggestResponse{} + mi := &file_protos_ai_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SuggestResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SuggestResponse) ProtoMessage() {} + +func (x *SuggestResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SuggestResponse.ProtoReflect.Descriptor instead. +func (*SuggestResponse) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{7} +} + +func (x *SuggestResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *SuggestResponse) GetRequest() string { + if x != nil { + return x.Request + } + return "" +} + +type ClearHistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearHistoryRequest) Reset() { + *x = ClearHistoryRequest{} + mi := &file_protos_ai_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearHistoryRequest) ProtoMessage() {} + +func (x *ClearHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearHistoryRequest.ProtoReflect.Descriptor instead. +func (*ClearHistoryRequest) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{8} +} + +func (x *ClearHistoryRequest) GetUid() int64 { + if x != nil { + return x.Uid + } + return 0 +} + +type ClearHistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearHistoryResponse) Reset() { + *x = ClearHistoryResponse{} + mi := &file_protos_ai_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearHistoryResponse) ProtoMessage() {} + +func (x *ClearHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_ai_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearHistoryResponse.ProtoReflect.Descriptor instead. +func (*ClearHistoryResponse) Descriptor() ([]byte, []int) { + return file_protos_ai_proto_rawDescGZIP(), []int{9} +} + +func (x *ClearHistoryResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +var File_protos_ai_proto protoreflect.FileDescriptor + +var file_protos_ai_proto_rawDesc = string([]byte{ + 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x03, 0x70, 0x6b, 0x67, 0x22, 0x3e, 0x0a, 0x14, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x70, 0x72, 0x6f, 0x6d, 0x74, 0x22, 0x29, 0x0a, 0x15, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, + 0x6c, 0x22, 0x57, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x4d, 0x6f, 0x64, 0x65, + 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3f, 0x0a, 0x13, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, + 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x17, 0x0a, 0x15, 0x47, + 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x58, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x1f, 0x0a, + 0x0b, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x3c, + 0x0a, 0x0e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, + 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x0f, + 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, + 0x18, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x27, 0x0a, 0x13, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, + 0x69, 0x64, 0x22, 0x26, 0x0a, 0x14, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x2a, 0x43, 0x0a, 0x09, 0x4d, 0x6f, + 0x64, 0x65, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x4d, 0x4f, 0x44, 0x45, 0x4c, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, + 0x0a, 0x0b, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x10, 0x01, 0x12, + 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x58, 0x54, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x10, 0x02, 0x32, + 0xd7, 0x02, 0x0a, 0x02, 0x41, 0x69, 0x12, 0x37, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, + 0x67, 0x65, 0x73, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x6b, 0x67, 0x2e, + 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, + 0x18, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x70, 0x6b, 0x67, 0x2e, + 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x40, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x17, + 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, + 0x67, 0x65, 0x12, 0x19, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, + 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, + 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x14, 0x5a, 0x12, 0x61, 0x6c, 0x67, + 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x61, 0x69, 0x2e, 0x76, 0x31, 0x3b, 0x61, 0x69, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_protos_ai_proto_rawDescOnce sync.Once + file_protos_ai_proto_rawDescData []byte +) + +func file_protos_ai_proto_rawDescGZIP() []byte { + file_protos_ai_proto_rawDescOnce.Do(func() { + file_protos_ai_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_ai_proto_rawDesc), len(file_protos_ai_proto_rawDesc))) + }) + return file_protos_ai_proto_rawDescData +} + +var file_protos_ai_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_protos_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_protos_ai_proto_goTypes = []any{ + (ModelType)(0), // 0: pkg.ModelType + (*GenerateImageRequest)(nil), // 1: pkg.GenerateImageRequest + (*GenerateImageResponse)(nil), // 2: pkg.GenerateImageResponse + (*ChangeModelRequest)(nil), // 3: pkg.ChangeModelRequest + (*ChangeModelResponse)(nil), // 4: pkg.ChangeModelResponse + (*GetInformationRequest)(nil), // 5: pkg.GetInformationRequest + (*GetInformationResponse)(nil), // 6: pkg.GetInformationResponse + (*SuggestRequest)(nil), // 7: pkg.SuggestRequest + (*SuggestResponse)(nil), // 8: pkg.SuggestResponse + (*ClearHistoryRequest)(nil), // 9: pkg.ClearHistoryRequest + (*ClearHistoryResponse)(nil), // 10: pkg.ClearHistoryResponse +} +var file_protos_ai_proto_depIdxs = []int32{ + 0, // 0: pkg.ChangeModelRequest.type:type_name -> pkg.ModelType + 7, // 1: pkg.Ai.GetSuggest:input_type -> pkg.SuggestRequest + 9, // 2: pkg.Ai.ClearHistory:input_type -> pkg.ClearHistoryRequest + 5, // 3: pkg.Ai.GetInformation:input_type -> pkg.GetInformationRequest + 3, // 4: pkg.Ai.ChangeModel:input_type -> pkg.ChangeModelRequest + 1, // 5: pkg.Ai.GenerateImage:input_type -> pkg.GenerateImageRequest + 8, // 6: pkg.Ai.GetSuggest:output_type -> pkg.SuggestResponse + 10, // 7: pkg.Ai.ClearHistory:output_type -> pkg.ClearHistoryResponse + 6, // 8: pkg.Ai.GetInformation:output_type -> pkg.GetInformationResponse + 4, // 9: pkg.Ai.ChangeModel:output_type -> pkg.ChangeModelResponse + 2, // 10: pkg.Ai.GenerateImage:output_type -> pkg.GenerateImageResponse + 6, // [6:11] is the sub-list for method output_type + 1, // [1:6] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_protos_ai_proto_init() } +func file_protos_ai_proto_init() { + if File_protos_ai_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_ai_proto_rawDesc), len(file_protos_ai_proto_rawDesc)), + NumEnums: 1, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_protos_ai_proto_goTypes, + DependencyIndexes: file_protos_ai_proto_depIdxs, + EnumInfos: file_protos_ai_proto_enumTypes, + MessageInfos: file_protos_ai_proto_msgTypes, + }.Build() + File_protos_ai_proto = out.File + file_protos_ai_proto_goTypes = nil + file_protos_ai_proto_depIdxs = nil +} diff --git a/protos/ai.proto b/protos/ai.proto new file mode 100644 index 0000000..d7500ba --- /dev/null +++ b/protos/ai.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package pkg; +option go_package = "algobot.ai.v1;aiv1"; + +service Ai { + rpc GetSuggest (SuggestRequest) returns (SuggestResponse); + rpc ClearHistory (ClearHistoryRequest) returns (ClearHistoryResponse); + rpc GetInformation (GetInformationRequest) returns (GetInformationResponse); + rpc ChangeModel (ChangeModelRequest) returns (ChangeModelResponse); + rpc GenerateImage (GenerateImageRequest) returns (GenerateImageResponse); +} + +message GenerateImageRequest { + int64 uid = 1; + string promt = 2; +} +message GenerateImageResponse { + string url = 1; +} + +enum ModelType { + MODEL_UNSPECIFIED = 0; + IMAGE_MODEL = 1; + TEXT_MODEL = 2; +} + +message ChangeModelRequest { + ModelType type = 1; + string model_name = 2; +} +message ChangeModelResponse { + bool ok = 1; + string message = 2; +} + +message GetInformationRequest {} +message GetInformationResponse { + string chat_model = 1; + string image_model = 2; +} + +message SuggestRequest { + int64 uid = 1; + string suggest = 2; +} + +message SuggestResponse { + bool ok = 1; + string request = 2; +} + +message ClearHistoryRequest { + int64 uid = 1; +} +message ClearHistoryResponse { + bool ok = 1; +} \ No newline at end of file diff --git a/protos/ai_grpc.pb.go b/protos/ai_grpc.pb.go new file mode 100644 index 0000000..b7274ca --- /dev/null +++ b/protos/ai_grpc.pb.go @@ -0,0 +1,273 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: protos/ai.proto + +package aiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Ai_GetSuggest_FullMethodName = "/pkg.Ai/GetSuggest" + Ai_ClearHistory_FullMethodName = "/pkg.Ai/ClearHistory" + Ai_GetInformation_FullMethodName = "/pkg.Ai/GetInformation" + Ai_ChangeModel_FullMethodName = "/pkg.Ai/ChangeModel" + Ai_GenerateImage_FullMethodName = "/pkg.Ai/GenerateImage" +) + +// AiClient is the client API for Ai service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AiClient interface { + GetSuggest(ctx context.Context, in *SuggestRequest, opts ...grpc.CallOption) (*SuggestResponse, error) + ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*ClearHistoryResponse, error) + GetInformation(ctx context.Context, in *GetInformationRequest, opts ...grpc.CallOption) (*GetInformationResponse, error) + ChangeModel(ctx context.Context, in *ChangeModelRequest, opts ...grpc.CallOption) (*ChangeModelResponse, error) + GenerateImage(ctx context.Context, in *GenerateImageRequest, opts ...grpc.CallOption) (*GenerateImageResponse, error) +} + +type aiClient struct { + cc grpc.ClientConnInterface +} + +func NewAiClient(cc grpc.ClientConnInterface) AiClient { + return &aiClient{cc} +} + +func (c *aiClient) GetSuggest(ctx context.Context, in *SuggestRequest, opts ...grpc.CallOption) (*SuggestResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SuggestResponse) + err := c.cc.Invoke(ctx, Ai_GetSuggest_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aiClient) ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*ClearHistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClearHistoryResponse) + err := c.cc.Invoke(ctx, Ai_ClearHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aiClient) GetInformation(ctx context.Context, in *GetInformationRequest, opts ...grpc.CallOption) (*GetInformationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetInformationResponse) + err := c.cc.Invoke(ctx, Ai_GetInformation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aiClient) ChangeModel(ctx context.Context, in *ChangeModelRequest, opts ...grpc.CallOption) (*ChangeModelResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ChangeModelResponse) + err := c.cc.Invoke(ctx, Ai_ChangeModel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aiClient) GenerateImage(ctx context.Context, in *GenerateImageRequest, opts ...grpc.CallOption) (*GenerateImageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GenerateImageResponse) + err := c.cc.Invoke(ctx, Ai_GenerateImage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AiServer is the server API for Ai service. +// All implementations must embed UnimplementedAiServer +// for forward compatibility. +type AiServer interface { + GetSuggest(context.Context, *SuggestRequest) (*SuggestResponse, error) + ClearHistory(context.Context, *ClearHistoryRequest) (*ClearHistoryResponse, error) + GetInformation(context.Context, *GetInformationRequest) (*GetInformationResponse, error) + ChangeModel(context.Context, *ChangeModelRequest) (*ChangeModelResponse, error) + GenerateImage(context.Context, *GenerateImageRequest) (*GenerateImageResponse, error) + mustEmbedUnimplementedAiServer() +} + +// UnimplementedAiServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAiServer struct{} + +func (UnimplementedAiServer) GetSuggest(context.Context, *SuggestRequest) (*SuggestResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSuggest not implemented") +} +func (UnimplementedAiServer) ClearHistory(context.Context, *ClearHistoryRequest) (*ClearHistoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented") +} +func (UnimplementedAiServer) GetInformation(context.Context, *GetInformationRequest) (*GetInformationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetInformation not implemented") +} +func (UnimplementedAiServer) ChangeModel(context.Context, *ChangeModelRequest) (*ChangeModelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ChangeModel not implemented") +} +func (UnimplementedAiServer) GenerateImage(context.Context, *GenerateImageRequest) (*GenerateImageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GenerateImage not implemented") +} +func (UnimplementedAiServer) mustEmbedUnimplementedAiServer() {} +func (UnimplementedAiServer) testEmbeddedByValue() {} + +// UnsafeAiServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AiServer will +// result in compilation errors. +type UnsafeAiServer interface { + mustEmbedUnimplementedAiServer() +} + +func RegisterAiServer(s grpc.ServiceRegistrar, srv AiServer) { + // If the following call pancis, it indicates UnimplementedAiServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Ai_ServiceDesc, srv) +} + +func _Ai_GetSuggest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SuggestRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).GetSuggest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_GetSuggest_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).GetSuggest(ctx, req.(*SuggestRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ai_ClearHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClearHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).ClearHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_ClearHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).ClearHistory(ctx, req.(*ClearHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ai_GetInformation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetInformationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).GetInformation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_GetInformation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).GetInformation(ctx, req.(*GetInformationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ai_ChangeModel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ChangeModelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).ChangeModel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_ChangeModel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).ChangeModel(ctx, req.(*ChangeModelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ai_GenerateImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenerateImageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).GenerateImage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_GenerateImage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).GenerateImage(ctx, req.(*GenerateImageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Ai_ServiceDesc is the grpc.ServiceDesc for Ai service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Ai_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "pkg.Ai", + HandlerType: (*AiServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetSuggest", + Handler: _Ai_GetSuggest_Handler, + }, + { + MethodName: "ClearHistory", + Handler: _Ai_ClearHistory_Handler, + }, + { + MethodName: "GetInformation", + Handler: _Ai_GetInformation_Handler, + }, + { + MethodName: "ChangeModel", + Handler: _Ai_ChangeModel_Handler, + }, + { + MethodName: "GenerateImage", + Handler: _Ai_GenerateImage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "protos/ai.proto", +} diff --git a/test/telegram/handlers/ai_test.go b/test/telegram/handlers/ai_test.go index 8a4c5da..73f7f30 100644 --- a/test/telegram/handlers/ai_test.go +++ b/test/telegram/handlers/ai_test.go @@ -1,6 +1,7 @@ package test import ( + "algobot/internal/domain/models" "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/fsm" "algobot/internal/telegram/handlers/text" @@ -27,14 +28,14 @@ func TestAI(t *testing.T) { mctx.EXPECT().Get("trace_id").Return("a-1").AnyTimes() t.Run("happy path", func(t *testing.T) { - aiRet := text.AIInfo{ + aiRet := models.AIInfo{ TextModel: "1", ImageModel: "1", } gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), - ai.EXPECT().GetAIInfo().Return(aiRet, nil).Times(1), + ai.EXPECT().GetAIInfo("a-1").Return(aiRet, nil).Times(1), stater.EXPECT().SetState(int64(1), fsm.ChattingAI).Times(1), mctx.EXPECT().Send(text.GetAIMessage(aiRet), keyboards.RejectKeyboard()).Times(1), ) @@ -42,12 +43,12 @@ func TestAI(t *testing.T) { assert.NoError(t, err) }) t.Run("GetAIInfo return err", func(t *testing.T) { - aiRet := text.AIInfo{} + aiRet := models.AIInfo{} aiErr := errors.New("GetAIInfo error") gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), - ai.EXPECT().GetAIInfo().Return(aiRet, aiErr).Times(1), + ai.EXPECT().GetAIInfo("a-1").Return(aiRet, aiErr).Times(1), mctx.EXPECT().Send("Упс, AI сейчас не работает!").Times(1), ) err := h(mctx) From b6cbad8fc3967989dc3213c1201f5a5ba7275066 Mon Sep 17 00:00:00 2001 From: danil227pavlov Date: Fri, 4 Apr 2025 18:02:36 +0300 Subject: [PATCH 20/44] fix communication with grpc --- internal/telegram/handlers/text/ai.go | 10 +- protos/ai.pb.go | 161 +++++++++++--------------- protos/ai.proto | 2 +- protos/ai_grpc.pb.go | 14 +-- test/telegram/handlers/ai_test.go | 2 +- 5 files changed, 83 insertions(+), 106 deletions(-) diff --git a/internal/telegram/handlers/text/ai.go b/internal/telegram/handlers/text/ai.go index dfe6e28..073016b 100644 --- a/internal/telegram/handlers/text/ai.go +++ b/internal/telegram/handlers/text/ai.go @@ -35,20 +35,22 @@ func NewAI(ai AIInformer, log *slog.Logger, stater AIStater) telebot.HandlerFunc } stater.SetState(uid, fsm.ChattingAI) - return ctx.Send(GetAIMessage(info), keyboards.RejectKeyboard()) + return ctx.Send(GetAIMessage(info), keyboards.RejectKeyboard(), telebot.ModeMarkdown) } } func GetAIMessage(info models.AIInfo) string { sb := strings.Builder{} sb.WriteString("Информация о включенных моделях:\n\n") - sb.WriteString("Текст: ") + sb.WriteString("***Текст:*** ") sb.WriteString(info.TextModel) - sb.WriteString("\nИзображение: ") + sb.WriteString(" 🗒\n***Изображение:*** ") sb.WriteString(info.ImageModel) - sb.WriteString("\n\n") + sb.WriteString(" 🖼\n\n") + sb.WriteString("```guide\n") sb.WriteString("/reset - отчистить память модели") sb.WriteString("\n/image promt - сгенерировать изображение") + sb.WriteString("\n```") sb.WriteString("\nДля текстового запроса - просто напиши в чат") return sb.String() } diff --git a/protos/ai.pb.go b/protos/ai.pb.go index d495c8e..95d7593 100644 --- a/protos/ai.pb.go +++ b/protos/ai.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.5 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v6.30.0 // source: protos/ai.proto package aiv1 @@ -168,7 +168,7 @@ func (x *GenerateImageResponse) GetUrl() string { type ChangeModelRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Type ModelType `protobuf:"varint,1,opt,name=type,proto3,enum=pkg.ModelType" json:"type,omitempty"` + Type ModelType `protobuf:"varint,1,opt,name=type,proto3,enum=pypkg.ModelType" json:"type,omitempty"` ModelName string `protobuf:"bytes,2,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -552,74 +552,49 @@ func (x *ClearHistoryResponse) GetOk() bool { var File_protos_ai_proto protoreflect.FileDescriptor -var file_protos_ai_proto_rawDesc = string([]byte{ - 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x03, 0x70, 0x6b, 0x67, 0x22, 0x3e, 0x0a, 0x14, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, - 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x70, 0x72, 0x6f, 0x6d, 0x74, 0x22, 0x29, 0x0a, 0x15, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, - 0x6c, 0x22, 0x57, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x4d, 0x6f, 0x64, 0x65, - 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3f, 0x0a, 0x13, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, - 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x17, 0x0a, 0x15, 0x47, - 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x58, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x1f, 0x0a, - 0x0b, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x3c, - 0x0a, 0x0e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, - 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x0f, - 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, - 0x18, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x27, 0x0a, 0x13, 0x43, 0x6c, 0x65, - 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, - 0x69, 0x64, 0x22, 0x26, 0x0a, 0x14, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, - 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x2a, 0x43, 0x0a, 0x09, 0x4d, 0x6f, - 0x64, 0x65, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x4d, 0x4f, 0x44, 0x45, 0x4c, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, - 0x0a, 0x0b, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x10, 0x01, 0x12, - 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x58, 0x54, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x10, 0x02, 0x32, - 0xd7, 0x02, 0x0a, 0x02, 0x41, 0x69, 0x12, 0x37, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, - 0x67, 0x65, 0x73, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x6b, 0x67, 0x2e, - 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x43, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, - 0x18, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, - 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x70, 0x6b, 0x67, 0x2e, - 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x40, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x17, - 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x12, 0x19, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, - 0x70, 0x6b, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x14, 0x5a, 0x12, 0x61, 0x6c, 0x67, - 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x61, 0x69, 0x2e, 0x76, 0x31, 0x3b, 0x61, 0x69, 0x76, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) +const file_protos_ai_proto_rawDesc = "" + + "\n" + + "\x0fprotos/ai.proto\x12\x05pypkg\">\n" + + "\x14GenerateImageRequest\x12\x10\n" + + "\x03uid\x18\x01 \x01(\x03R\x03uid\x12\x14\n" + + "\x05promt\x18\x02 \x01(\tR\x05promt\")\n" + + "\x15GenerateImageResponse\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\"Y\n" + + "\x12ChangeModelRequest\x12$\n" + + "\x04type\x18\x01 \x01(\x0e2\x10.pypkg.ModelTypeR\x04type\x12\x1d\n" + + "\n" + + "model_name\x18\x02 \x01(\tR\tmodelName\"?\n" + + "\x13ChangeModelResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\x17\n" + + "\x15GetInformationRequest\"X\n" + + "\x16GetInformationResponse\x12\x1d\n" + + "\n" + + "chat_model\x18\x01 \x01(\tR\tchatModel\x12\x1f\n" + + "\vimage_model\x18\x02 \x01(\tR\n" + + "imageModel\"<\n" + + "\x0eSuggestRequest\x12\x10\n" + + "\x03uid\x18\x01 \x01(\x03R\x03uid\x12\x18\n" + + "\asuggest\x18\x02 \x01(\tR\asuggest\";\n" + + "\x0fSuggestResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x18\n" + + "\arequest\x18\x02 \x01(\tR\arequest\"'\n" + + "\x13ClearHistoryRequest\x12\x10\n" + + "\x03uid\x18\x01 \x01(\x03R\x03uid\"&\n" + + "\x14ClearHistoryResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok*C\n" + + "\tModelType\x12\x15\n" + + "\x11MODEL_UNSPECIFIED\x10\x00\x12\x0f\n" + + "\vIMAGE_MODEL\x10\x01\x12\x0e\n" + + "\n" + + "TEXT_MODEL\x10\x022\xeb\x02\n" + + "\x02Ai\x12;\n" + + "\n" + + "GetSuggest\x12\x15.pypkg.SuggestRequest\x1a\x16.pypkg.SuggestResponse\x12G\n" + + "\fClearHistory\x12\x1a.pypkg.ClearHistoryRequest\x1a\x1b.pypkg.ClearHistoryResponse\x12M\n" + + "\x0eGetInformation\x12\x1c.pypkg.GetInformationRequest\x1a\x1d.pypkg.GetInformationResponse\x12D\n" + + "\vChangeModel\x12\x19.pypkg.ChangeModelRequest\x1a\x1a.pypkg.ChangeModelResponse\x12J\n" + + "\rGenerateImage\x12\x1b.pypkg.GenerateImageRequest\x1a\x1c.pypkg.GenerateImageResponseB\x14Z\x12algobot.ai.v1;aiv1b\x06proto3" var ( file_protos_ai_proto_rawDescOnce sync.Once @@ -636,30 +611,30 @@ func file_protos_ai_proto_rawDescGZIP() []byte { var file_protos_ai_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_protos_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_protos_ai_proto_goTypes = []any{ - (ModelType)(0), // 0: pkg.ModelType - (*GenerateImageRequest)(nil), // 1: pkg.GenerateImageRequest - (*GenerateImageResponse)(nil), // 2: pkg.GenerateImageResponse - (*ChangeModelRequest)(nil), // 3: pkg.ChangeModelRequest - (*ChangeModelResponse)(nil), // 4: pkg.ChangeModelResponse - (*GetInformationRequest)(nil), // 5: pkg.GetInformationRequest - (*GetInformationResponse)(nil), // 6: pkg.GetInformationResponse - (*SuggestRequest)(nil), // 7: pkg.SuggestRequest - (*SuggestResponse)(nil), // 8: pkg.SuggestResponse - (*ClearHistoryRequest)(nil), // 9: pkg.ClearHistoryRequest - (*ClearHistoryResponse)(nil), // 10: pkg.ClearHistoryResponse + (ModelType)(0), // 0: pypkg.ModelType + (*GenerateImageRequest)(nil), // 1: pypkg.GenerateImageRequest + (*GenerateImageResponse)(nil), // 2: pypkg.GenerateImageResponse + (*ChangeModelRequest)(nil), // 3: pypkg.ChangeModelRequest + (*ChangeModelResponse)(nil), // 4: pypkg.ChangeModelResponse + (*GetInformationRequest)(nil), // 5: pypkg.GetInformationRequest + (*GetInformationResponse)(nil), // 6: pypkg.GetInformationResponse + (*SuggestRequest)(nil), // 7: pypkg.SuggestRequest + (*SuggestResponse)(nil), // 8: pypkg.SuggestResponse + (*ClearHistoryRequest)(nil), // 9: pypkg.ClearHistoryRequest + (*ClearHistoryResponse)(nil), // 10: pypkg.ClearHistoryResponse } var file_protos_ai_proto_depIdxs = []int32{ - 0, // 0: pkg.ChangeModelRequest.type:type_name -> pkg.ModelType - 7, // 1: pkg.Ai.GetSuggest:input_type -> pkg.SuggestRequest - 9, // 2: pkg.Ai.ClearHistory:input_type -> pkg.ClearHistoryRequest - 5, // 3: pkg.Ai.GetInformation:input_type -> pkg.GetInformationRequest - 3, // 4: pkg.Ai.ChangeModel:input_type -> pkg.ChangeModelRequest - 1, // 5: pkg.Ai.GenerateImage:input_type -> pkg.GenerateImageRequest - 8, // 6: pkg.Ai.GetSuggest:output_type -> pkg.SuggestResponse - 10, // 7: pkg.Ai.ClearHistory:output_type -> pkg.ClearHistoryResponse - 6, // 8: pkg.Ai.GetInformation:output_type -> pkg.GetInformationResponse - 4, // 9: pkg.Ai.ChangeModel:output_type -> pkg.ChangeModelResponse - 2, // 10: pkg.Ai.GenerateImage:output_type -> pkg.GenerateImageResponse + 0, // 0: pypkg.ChangeModelRequest.type:type_name -> pypkg.ModelType + 7, // 1: pypkg.Ai.GetSuggest:input_type -> pypkg.SuggestRequest + 9, // 2: pypkg.Ai.ClearHistory:input_type -> pypkg.ClearHistoryRequest + 5, // 3: pypkg.Ai.GetInformation:input_type -> pypkg.GetInformationRequest + 3, // 4: pypkg.Ai.ChangeModel:input_type -> pypkg.ChangeModelRequest + 1, // 5: pypkg.Ai.GenerateImage:input_type -> pypkg.GenerateImageRequest + 8, // 6: pypkg.Ai.GetSuggest:output_type -> pypkg.SuggestResponse + 10, // 7: pypkg.Ai.ClearHistory:output_type -> pypkg.ClearHistoryResponse + 6, // 8: pypkg.Ai.GetInformation:output_type -> pypkg.GetInformationResponse + 4, // 9: pypkg.Ai.ChangeModel:output_type -> pypkg.ChangeModelResponse + 2, // 10: pypkg.Ai.GenerateImage:output_type -> pypkg.GenerateImageResponse 6, // [6:11] is the sub-list for method output_type 1, // [1:6] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name diff --git a/protos/ai.proto b/protos/ai.proto index d7500ba..52e0bcb 100644 --- a/protos/ai.proto +++ b/protos/ai.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package pkg; +package pypkg; option go_package = "algobot.ai.v1;aiv1"; service Ai { diff --git a/protos/ai_grpc.pb.go b/protos/ai_grpc.pb.go index b7274ca..2c84444 100644 --- a/protos/ai_grpc.pb.go +++ b/protos/ai_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.30.0 // source: protos/ai.proto package aiv1 @@ -19,11 +19,11 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Ai_GetSuggest_FullMethodName = "/pkg.Ai/GetSuggest" - Ai_ClearHistory_FullMethodName = "/pkg.Ai/ClearHistory" - Ai_GetInformation_FullMethodName = "/pkg.Ai/GetInformation" - Ai_ChangeModel_FullMethodName = "/pkg.Ai/ChangeModel" - Ai_GenerateImage_FullMethodName = "/pkg.Ai/GenerateImage" + Ai_GetSuggest_FullMethodName = "/pypkg.Ai/GetSuggest" + Ai_ClearHistory_FullMethodName = "/pypkg.Ai/ClearHistory" + Ai_GetInformation_FullMethodName = "/pypkg.Ai/GetInformation" + Ai_ChangeModel_FullMethodName = "/pypkg.Ai/ChangeModel" + Ai_GenerateImage_FullMethodName = "/pypkg.Ai/GenerateImage" ) // AiClient is the client API for Ai service. @@ -244,7 +244,7 @@ func _Ai_GenerateImage_Handler(srv interface{}, ctx context.Context, dec func(in // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var Ai_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "pkg.Ai", + ServiceName: "pypkg.Ai", HandlerType: (*AiServer)(nil), Methods: []grpc.MethodDesc{ { diff --git a/test/telegram/handlers/ai_test.go b/test/telegram/handlers/ai_test.go index 73f7f30..44bfb95 100644 --- a/test/telegram/handlers/ai_test.go +++ b/test/telegram/handlers/ai_test.go @@ -37,7 +37,7 @@ func TestAI(t *testing.T) { mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), ai.EXPECT().GetAIInfo("a-1").Return(aiRet, nil).Times(1), stater.EXPECT().SetState(int64(1), fsm.ChattingAI).Times(1), - mctx.EXPECT().Send(text.GetAIMessage(aiRet), keyboards.RejectKeyboard()).Times(1), + mctx.EXPECT().Send(text.GetAIMessage(aiRet), keyboards.RejectKeyboard(), tele.ModeMarkdown).Times(1), ) err := h(mctx) assert.NoError(t, err) From 938ba81b9234f9a94e81c48e3036de1bf2b679d9 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 7 Apr 2025 13:55:22 +0300 Subject: [PATCH 21/44] add generate image method + tests --- go.mod | 2 +- go.sum | 2 + internal/app/telegram/app.go | 15 ++- internal/services/grpc/ai.go | 44 +++++-- internal/services/grpc/generateImage.go | 32 +++++ internal/services/grpc/resetHistory.go | 34 ++++++ .../telegram/handlers/text/generateImage.go | 49 ++++++++ internal/telegram/handlers/text/reset.go | 32 +++++ test/mocks/services/mockgen.go | 2 + test/mocks/telegram/handlers/mockgen.go | 4 + test/mocks/telegram/mockgen.go | 1 + test/services/grpc_ai_test.go | 112 ++++++++++++++++++ test/telegram/handlers/reset_test.go | 47 ++++++++ 13 files changed, 363 insertions(+), 13 deletions(-) create mode 100644 internal/services/grpc/generateImage.go create mode 100644 internal/services/grpc/resetHistory.go create mode 100644 internal/telegram/handlers/text/generateImage.go create mode 100644 internal/telegram/handlers/text/reset.go create mode 100644 test/services/grpc_ai_test.go create mode 100644 test/telegram/handlers/reset_test.go diff --git a/go.mod b/go.mod index f730b06..ccee369 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module algobot go 1.23.3 require ( - github.com/LZTD1/telebot-context-router v1.0.1 + github.com/LZTD1/telebot-context-router v1.1.0 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jxskiss/base62 v1.1.0 diff --git a/go.sum b/go.sum index b38d73c..92aaf87 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/LZTD1/telebot-context-router v1.0.1 h1:iGrKhFevnXFtQre+3bum6g9AluMTE23IVz2A+cOZPYE= github.com/LZTD1/telebot-context-router v1.0.1/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= +github.com/LZTD1/telebot-context-router v1.1.0 h1:WCdHvW+dcgGU0NBWIq3Cc6X++UeWQ4BzQ6tr3AdHxfI= +github.com/LZTD1/telebot-context-router v1.1.0/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 2ca7255..e2c9edb 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -69,7 +69,10 @@ func New( groupServ := services.NewGroup(log, grGetter) stateMachine := memory.New() serdes := base62.NewSerdes(log) - grpc := grpc2.NewAIService(grpcCfg, log) + grpc := grpc2.NewAIService( + grpcCfg, + grpc2.WithLogger(log), + ) // initialize routes b.Use(trace.New(log)) @@ -103,6 +106,16 @@ func New( r.HandleFuncRegexpText(regexp.MustCompile(".+"), text.NewSendingCookie(log, cookieSetter, stateMachine)) }) + r.Group(func(r router.Router) { // Routes for ChattingAI state + r.Use(stater.New(stateMachine, fsm.ChattingAI)) + + // message + r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) + r.HandleFuncText("/reset", text.NewReset(grpc, log)) + r.HandleFuncRegexpText(regexp.MustCompile(`(?m)\/image\s(.+)$`), text.GenerateImage(grpc, log)) + + }) + r.NotFound(text.NewStart(stateMachine)) b.Handle(tele.OnText, r.ServeContext) diff --git a/internal/services/grpc/ai.go b/internal/services/grpc/ai.go index 3da91ec..480c045 100644 --- a/internal/services/grpc/ai.go +++ b/internal/services/grpc/ai.go @@ -3,32 +3,54 @@ package grpc import ( "algobot/internal/config" "algobot/internal/domain/models" + "algobot/internal/lib/logger/handlers/slogpretty" "algobot/internal/lib/logger/sl" aiv1 "algobot/protos" "context" + "errors" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "log/slog" - "time" ) +var ( + ErrNotValidResponse = errors.New("not valid response") +) + +type AiOption func(*AIService) + type AIService struct { - grpc aiv1.AiClient - timeout time.Duration - log *slog.Logger + grpc aiv1.AiClient + log *slog.Logger + cfg config.GRPC } -func NewAIService(grpcCfg config.GRPC, log *slog.Logger) *AIService { - conn, err := grpc.NewClient(fmt.Sprintf("%s:%s", grpcCfg.Host, grpcCfg.Port), grpc.WithTransportCredentials(insecure.NewCredentials())) +func NewAIService(cfg config.GRPC, fn ...func(*AIService)) *AIService { + conn, err := grpc.NewClient(fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { panic(err) } + svr := &AIService{ + grpc: aiv1.NewAiClient(conn), + log: slog.New(slogpretty.NewHandler(&slog.HandlerOptions{Level: slog.LevelDebug})), + cfg: cfg, + } - return &AIService{ - grpc: aiv1.NewAiClient(conn), - timeout: grpcCfg.Timeout, - log: log, + for _, o := range fn { + o(svr) + } + + return svr +} +func WithLogger(log *slog.Logger) func(*AIService) { + return func(s *AIService) { + s.log = log + } +} +func WithClient(client aiv1.AiClient) func(*AIService) { + return func(s *AIService) { + s.grpc = client } } @@ -39,7 +61,7 @@ func (a *AIService) GetAIInfo(traceID interface{}) (models.AIInfo, error) { slog.Any("trace_id", traceID), ) - ctx, cancel := context.WithTimeout(context.Background(), a.timeout) + ctx, cancel := context.WithTimeout(context.Background(), a.cfg.Timeout) defer cancel() information, err := a.grpc.GetInformation(ctx, &aiv1.GetInformationRequest{}) diff --git a/internal/services/grpc/generateImage.go b/internal/services/grpc/generateImage.go new file mode 100644 index 0000000..73cc120 --- /dev/null +++ b/internal/services/grpc/generateImage.go @@ -0,0 +1,32 @@ +package grpc + +import ( + "algobot/internal/lib/logger/sl" + aiv1 "algobot/protos" + "context" + "fmt" + "log/slog" +) + +func (a *AIService) GenerateImage(uid int64, promt string, traceID interface{}) (string, error) { + const op = "grpc.AIService.GenerateImage" + log := a.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + ctx, cancel := context.WithTimeout(context.Background(), a.cfg.Timeout) + defer cancel() + + resp, err := a.grpc.GenerateImage(ctx, &aiv1.GenerateImageRequest{ + Uid: uid, + Promt: promt, + }) + + if err != nil { + log.Warn("error while calling gRPC GenerateImage", sl.Err(err)) + return "", fmt.Errorf("%s error while calling gRPC GenerateImage: %w", op, err) + } + + return resp.GetUrl(), nil +} diff --git a/internal/services/grpc/resetHistory.go b/internal/services/grpc/resetHistory.go new file mode 100644 index 0000000..c10189b --- /dev/null +++ b/internal/services/grpc/resetHistory.go @@ -0,0 +1,34 @@ +package grpc + +import ( + "algobot/internal/lib/logger/sl" + aiv1 "algobot/protos" + "context" + "fmt" + "log/slog" +) + +func (a *AIService) ResetHistory(uid int64, traceID interface{}) error { + const op = "grpc.AIService.ResetHistory" + log := a.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + ctx, cancel := context.WithTimeout(context.Background(), a.cfg.Timeout) + defer cancel() + + ok, err := a.grpc.ClearHistory(ctx, &aiv1.ClearHistoryRequest{ + Uid: uid, + }) + if err != nil { + log.Warn("error while calling gRPC ClearHistory", sl.Err(err)) + return fmt.Errorf("%s error while calling gRPC ClearHistory: %w", op, err) + } + if !ok.GetOk() { + log.Warn("error while checking status grpc, not ok", slog.Bool("ok", ok.GetOk())) + return fmt.Errorf("%s error while checking status grpc, not ok: %w", op, ErrNotValidResponse) + } + + return nil +} diff --git a/internal/telegram/handlers/text/generateImage.go b/internal/telegram/handlers/text/generateImage.go new file mode 100644 index 0000000..aebc778 --- /dev/null +++ b/internal/telegram/handlers/text/generateImage.go @@ -0,0 +1,49 @@ +package text + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type GeneratorImage interface { + GenerateImage(uid int64, promt string, traceID interface{}) (string, error) +} + +func GenerateImage(generator GeneratorImage, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.GenerateImage" + uid := ctx.Sender().ID + traceID := ctx.Get("trace_id") + promt := getPromtFromMessage(ctx.Message().Text) + log = log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + msg, err := ctx.Bot().Send(telebot.ChatID(uid), "⚙️ Генерирую изображение ...") + if err != nil { + log.Warn("failed to send prepare msg", sl.Err(err)) + return fmt.Errorf("%s failed to send prepare msg: %w", op, err) + } + + imgURL, err := generator.GenerateImage(uid, promt, traceID) + if err != nil { + log.Warn("failed to generate image", sl.Err(err)) + ctx.Bot().Edit(msg, "⚠️ К сожалению, я не смог сгенерировать изображение, попробуйте снова чуть позже") + return fmt.Errorf("%s failed to generate image: %w", op, err) + } + + _, err = ctx.Bot().Edit(msg, &telebot.Photo{ + File: telebot.FromURL(imgURL), + }) + + return err + } +} + +func getPromtFromMessage(text string) string { + return strings.TrimSpace(strings.TrimLeft(text, "/image")) +} diff --git a/internal/telegram/handlers/text/reset.go b/internal/telegram/handlers/text/reset.go new file mode 100644 index 0000000..321bca6 --- /dev/null +++ b/internal/telegram/handlers/text/reset.go @@ -0,0 +1,32 @@ +package text + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type Reseter interface { + ResetHistory(uid int64, traceID interface{}) error +} + +func NewReset(reseter Reseter, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewReset" + + uid := ctx.Sender().ID + traceID := ctx.Get("trace_id") + log = log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + if err := reseter.ResetHistory(uid, traceID); err != nil { + log.Warn("failed to reset history", sl.Err(err)) + return fmt.Errorf("%s failed to reset history: %w", op, err) + } + + return ctx.Send("История успешно отчищена") + } +} diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index 9760188..d6f0651 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -1,3 +1,5 @@ package mocks //go:generate mockgen -destination=./groupGetter_mock.go -package=mocks algobot/internal/services GroupGetter + +//go:generate mockgen -destination=./aiClient_mock.go -package=mocks algobot/protos AiClient diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 48a45f0..1ea682c 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -12,3 +12,7 @@ package mocks //go:generate mockgen -destination=./grouper_mock.go -package=mocks algobot/internal/telegram/handlers/text Grouper //go:generate mockgen -destination=./groupSerializer_mock.go -package=mocks algobot/internal/telegram/handlers/text GroupSerializer + +//go:generate mockgen -destination=./reseter_mock.go -package=mocks algobot/internal/telegram/handlers/text Reseter + +//go:generate mockgen -destination=./generatorImage_mock.go -package=mocks algobot/internal/telegram/handlers/text GeneratorImage diff --git a/test/mocks/telegram/mockgen.go b/test/mocks/telegram/mockgen.go index 0d3dbc7..be752fc 100644 --- a/test/mocks/telegram/mockgen.go +++ b/test/mocks/telegram/mockgen.go @@ -1,3 +1,4 @@ package mocks //go:generate mockgen -destination=./context_mock.go -package=mocks gopkg.in/telebot.v4 Context +//go:generate mockgen -destination=./api_mock.go -package=mocks gopkg.in/telebot.v4 API diff --git a/test/services/grpc_ai_test.go b/test/services/grpc_ai_test.go new file mode 100644 index 0000000..f210240 --- /dev/null +++ b/test/services/grpc_ai_test.go @@ -0,0 +1,112 @@ +package test + +import ( + "algobot/internal/config" + "algobot/internal/domain/models" + "algobot/internal/services/grpc" + aiv1 "algobot/protos" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/services" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func TestGRPCAI(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + aiClient := mocks2.NewMockAiClient(ctrl) + + svc := grpc.NewAIService(config.GRPC{ + Host: "0.0.0.0", + Port: "1111", + Timeout: 100 * time.Millisecond, + }, grpc.WithLogger(log), grpc.WithClient(aiClient)) + + t.Run("GetAIInfo", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + aiClient.EXPECT().GetInformation(gomock.Any(), gomock.Any()).Return( + &aiv1.GetInformationResponse{ + ChatModel: "chat", + ImageModel: "image", + }, nil, + ).Times(1) + + info, err := svc.GetAIInfo("") + assert.NoError(t, err) + assert.Equal(t, models.AIInfo{ + TextModel: "chat", + ImageModel: "image", + }, info) + }) + t.Run("GetInformation return err", func(t *testing.T) { + errExp := errors.New("GetInformation err") + aiClient.EXPECT().GetInformation(gomock.Any(), gomock.Any()).Return(nil, errExp).Times(1) + + _, err := svc.GetAIInfo("") + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + }) + t.Run("ResetHistory", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + aiClient.EXPECT().ClearHistory(gomock.Any(), &aiv1.ClearHistoryRequest{ + Uid: int64(1), + }).Return(&aiv1.ClearHistoryResponse{Ok: true}, nil).Times(1) + + err := svc.ResetHistory(1, "") + assert.NoError(t, err) + }) + t.Run("ClearHistory return err", func(t *testing.T) { + errExp := errors.New("ClearHistory err") + + aiClient.EXPECT().ClearHistory(gomock.Any(), &aiv1.ClearHistoryRequest{ + Uid: int64(1), + }).Return(&aiv1.ClearHistoryResponse{Ok: false}, errExp).Times(1) + + err := svc.ResetHistory(1, "") + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + t.Run("ClearHistory return false", func(t *testing.T) { + aiClient.EXPECT().ClearHistory(gomock.Any(), &aiv1.ClearHistoryRequest{ + Uid: int64(1), + }).Return(&aiv1.ClearHistoryResponse{Ok: false}, nil).Times(1) + + err := svc.ResetHistory(1, "") + assert.Error(t, err) + assert.ErrorIs(t, err, grpc.ErrNotValidResponse) + }) + }) + t.Run("GenerateImage", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + aiClient.EXPECT().GenerateImage(gomock.Any(), &aiv1.GenerateImageRequest{ + Uid: int64(1), + Promt: "firefox", + }).Return(&aiv1.GenerateImageResponse{ + Url: "https", + }, nil).Times(1) + + url, err := svc.GenerateImage(int64(1), "firefox", "") + assert.NoError(t, err) + assert.Equal(t, "https", url) + }) + t.Run("GenerateImage return err", func(t *testing.T) { + errExp := errors.New("GenerateImage err") + + aiClient.EXPECT().GenerateImage(gomock.Any(), &aiv1.GenerateImageRequest{ + Uid: int64(1), + Promt: "firefox", + }).Return(nil, errExp).Times(1) + + _, err := svc.GenerateImage(int64(1), "firefox", "") + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + }) + +} diff --git a/test/telegram/handlers/reset_test.go b/test/telegram/handlers/reset_test.go new file mode 100644 index 0000000..e4d3246 --- /dev/null +++ b/test/telegram/handlers/reset_test.go @@ -0,0 +1,47 @@ +package test + +import ( + "algobot/internal/telegram/handlers/text" + mocks2 "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestReset(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + reseter := mocks.NewMockReseter(ctrl) + log := mocks2.NewMockLogger() + mctx := mocks3.NewMockContext(ctrl) + + handler := text.NewReset(reseter, log) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + reseter.EXPECT().ResetHistory(int64(1), "").Return(nil).Times(1), + mctx.EXPECT().Send("История успешно отчищена").Return(nil).Times(1), + ) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("ResetHistory returns err", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + reseter.EXPECT().ResetHistory(int64(1), "").Return(errExp).Times(1), + ) + + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} From cd6e79e178679e8e7db1e7ba082f7e60e4a832c3 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 7 Apr 2025 15:37:05 +0300 Subject: [PATCH 22/44] add chat ai method + tests --- internal/app/telegram/app.go | 3 +- internal/services/grpc/chatAI.go | 35 +++++++++ internal/telegram/handlers/text/chatAI.go | 42 +++++++++++ .../telegram/handlers/text/generateImage.go | 2 +- test/mocks/telegram/handlers/mockgen.go | 2 + test/services/grpc_ai_test.go | 43 +++++++++++ test/telegram/handlers/chatAI_test.go | 59 +++++++++++++++ test/telegram/handlers/generateImage_test.go | 75 +++++++++++++++++++ 8 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 internal/services/grpc/chatAI.go create mode 100644 internal/telegram/handlers/text/chatAI.go create mode 100644 test/telegram/handlers/chatAI_test.go create mode 100644 test/telegram/handlers/generateImage_test.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index e2c9edb..310bf1c 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -112,7 +112,8 @@ func New( // message r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) r.HandleFuncText("/reset", text.NewReset(grpc, log)) - r.HandleFuncRegexpText(regexp.MustCompile(`(?m)\/image\s(.+)$`), text.GenerateImage(grpc, log)) + r.HandleFuncRegexpText(regexp.MustCompile(`^(?m)\/image\s(.+)$`), text.GenerateImage(grpc, log)) + r.HandleFuncRegexpText(regexp.MustCompile(`^[^/].*$`), text.ChatAI(grpc, log)) }) diff --git a/internal/services/grpc/chatAI.go b/internal/services/grpc/chatAI.go new file mode 100644 index 0000000..0cb5ec7 --- /dev/null +++ b/internal/services/grpc/chatAI.go @@ -0,0 +1,35 @@ +package grpc + +import ( + "algobot/internal/lib/logger/sl" + aiv1 "algobot/protos" + "context" + "fmt" + "log/slog" +) + +func (a *AIService) ChatAI(uid int64, message string, traceID interface{}) (string, error) { + const op = "grpc.AIService.ChatAI" + log := a.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + ctx, cancel := context.WithTimeout(context.Background(), a.cfg.Timeout) + defer cancel() + + resp, err := a.grpc.GetSuggest(ctx, &aiv1.SuggestRequest{ + Uid: uid, + Suggest: message, + }) + if err != nil { + log.Warn("error while calling gRPC GetSuggest", sl.Err(err)) + return "", fmt.Errorf("%s error while calling gRPC GetSuggest: %w", op, err) + } + if !resp.GetOk() { + log.Warn("error while checking status grpc, not ok", slog.Bool("ok", resp.GetOk())) + return "", fmt.Errorf("%s error while checking status grpc, not ok: %w", op, ErrNotValidResponse) + } + + return resp.GetRequest(), nil +} diff --git a/internal/telegram/handlers/text/chatAI.go b/internal/telegram/handlers/text/chatAI.go new file mode 100644 index 0000000..0b30b0d --- /dev/null +++ b/internal/telegram/handlers/text/chatAI.go @@ -0,0 +1,42 @@ +package text + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type Chatter interface { + ChatAI(uid int64, message string, traceID interface{}) (string, error) +} + +func ChatAI(chatter Chatter, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.ChatAI" + + uid := ctx.Sender().ID + message := ctx.Message().Text + traceID := ctx.Get("trace_id") + log = log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + msg, err := ctx.Bot().Reply(ctx.Message(), "⚙️ Думаю что ответить ...") + if err != nil { + log.Warn("failed to send prepare msg", sl.Err(err)) + return fmt.Errorf("%s failed to send prepare msg: %w", op, err) + } + + resp, err := chatter.ChatAI(uid, message, traceID) + if err != nil { + log.Warn("failed to ChatAI", sl.Err(err)) + ctx.Bot().Edit(msg, "⚠️ К сожалению, я не смог ответить на ваше сообщение, попробуйте снова чуть позже") + return fmt.Errorf("%s failed to chatting ai: %w", op, err) + } + + _, err = ctx.Bot().Edit(msg, resp, telebot.ModeMarkdown) + return err + } +} diff --git a/internal/telegram/handlers/text/generateImage.go b/internal/telegram/handlers/text/generateImage.go index aebc778..17269a3 100644 --- a/internal/telegram/handlers/text/generateImage.go +++ b/internal/telegram/handlers/text/generateImage.go @@ -23,7 +23,7 @@ func GenerateImage(generator GeneratorImage, log *slog.Logger) telebot.HandlerFu slog.Any("trace_id", traceID), ) - msg, err := ctx.Bot().Send(telebot.ChatID(uid), "⚙️ Генерирую изображение ...") + msg, err := ctx.Bot().Reply(ctx.Message(), "⚙️ Генерирую изображение ...") if err != nil { log.Warn("failed to send prepare msg", sl.Err(err)) return fmt.Errorf("%s failed to send prepare msg: %w", op, err) diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 1ea682c..cf57c14 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -16,3 +16,5 @@ package mocks //go:generate mockgen -destination=./reseter_mock.go -package=mocks algobot/internal/telegram/handlers/text Reseter //go:generate mockgen -destination=./generatorImage_mock.go -package=mocks algobot/internal/telegram/handlers/text GeneratorImage + +//go:generate mockgen -destination=./chatter_mock.go -package=mocks algobot/internal/telegram/handlers/text Chatter diff --git a/test/services/grpc_ai_test.go b/test/services/grpc_ai_test.go index f210240..c180c8a 100644 --- a/test/services/grpc_ai_test.go +++ b/test/services/grpc_ai_test.go @@ -108,5 +108,48 @@ func TestGRPCAI(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) + t.Run("GetSuggest", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + aiClient.EXPECT().GetSuggest(gomock.Any(), &aiv1.SuggestRequest{ + Uid: int64(1), + Suggest: "Suggest", + }).Return(&aiv1.SuggestResponse{ + Ok: true, + Request: "Request", + }, nil).Times(1) + + msg, err := svc.ChatAI(int64(1), "Suggest", "") + assert.NoError(t, err) + assert.Equal(t, "Request", msg) + }) + t.Run("GetSuggest return err", func(t *testing.T) { + errExp := errors.New("GetSuggest err") + + aiClient.EXPECT().GetSuggest(gomock.Any(), &aiv1.SuggestRequest{ + Uid: int64(1), + Suggest: "Suggest", + }).Return(&aiv1.SuggestResponse{ + Ok: false, + Request: "", + }, errExp).Times(1) + + _, err := svc.ChatAI(int64(1), "Suggest", "") + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + t.Run("ClearHistory return false", func(t *testing.T) { + aiClient.EXPECT().GetSuggest(gomock.Any(), &aiv1.SuggestRequest{ + Uid: int64(1), + Suggest: "Suggest", + }).Return(&aiv1.SuggestResponse{ + Ok: false, + Request: "", + }, nil).Times(1) + + _, err := svc.ChatAI(int64(1), "Suggest", "") + assert.Error(t, err) + assert.ErrorIs(t, err, grpc.ErrNotValidResponse) + }) + }) } diff --git a/test/telegram/handlers/chatAI_test.go b/test/telegram/handlers/chatAI_test.go new file mode 100644 index 0000000..6615c35 --- /dev/null +++ b/test/telegram/handlers/chatAI_test.go @@ -0,0 +1,59 @@ +package test + +import ( + "algobot/internal/telegram/handlers/text" + mocks2 "algobot/test/mocks" + mocks "algobot/test/mocks/telegram" + mocks3 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestChatAI(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mctx := mocks.NewMockContext(ctrl) + chatter := mocks3.NewMockChatter(ctrl) + log := mocks2.NewMockLogger() + mapi := mocks.NewMockAPI(ctrl) + + mctx.EXPECT().Bot().Return(mapi).AnyTimes() + handler := text.ChatAI(chatter, log) + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: "some"}).Times(1), + mctx.EXPECT().Get(gomock.Any()).Return("").Times(1), + + mctx.EXPECT().Message().Return(&tele.Message{ID: 1}).Times(1), + mapi.EXPECT().Reply(&tele.Message{ID: 1}, "⚙️ Думаю что ответить ...").Return(&tele.Message{ID: 2}, nil).Times(1), + + chatter.EXPECT().ChatAI(int64(1), "some", "").Return("msg", nil).Times(1), + + mapi.EXPECT().Edit(&tele.Message{ID: 2}, "msg", tele.ModeMarkdown).Return(&tele.Message{ID: 2}, nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("ChatAI returns err", func(t *testing.T) { + errExp := errors.New("") + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: "some"}).Times(1), + mctx.EXPECT().Get(gomock.Any()).Return("").Times(1), + + mctx.EXPECT().Message().Return(&tele.Message{ID: 1}).Times(1), + mapi.EXPECT().Reply(&tele.Message{ID: 1}, "⚙️ Думаю что ответить ...").Return(&tele.Message{ID: 2}, nil).Times(1), + + chatter.EXPECT().ChatAI(int64(1), "some", "").Return("", errExp).Times(1), + + mapi.EXPECT().Edit(&tele.Message{ID: 2}, "⚠️ К сожалению, я не смог ответить на ваше сообщение, попробуйте снова чуть позже").Return(&tele.Message{ID: 2}, nil).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} diff --git a/test/telegram/handlers/generateImage_test.go b/test/telegram/handlers/generateImage_test.go new file mode 100644 index 0000000..ca750dd --- /dev/null +++ b/test/telegram/handlers/generateImage_test.go @@ -0,0 +1,75 @@ +package test + +import ( + "algobot/internal/telegram/handlers/text" + mocks2 "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestGen(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + gen := mocks.NewMockGeneratorImage(ctrl) + log := mocks2.NewMockLogger() + mctx := mocks3.NewMockContext(ctrl) + mapi := mocks3.NewMockAPI(ctrl) + + handler := text.GenerateImage(gen, log) + + mctx.EXPECT().Bot().Return(mapi).AnyTimes() + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: "/image firefox"}).Times(1), + + mctx.EXPECT().Message().Return(&tele.Message{ID: 1}).Times(1), + mapi.EXPECT().Reply(&tele.Message{ID: 1}, "⚙️ Генерирую изображение ...").Return(&tele.Message{ID: 1}, nil).Times(1), + + gen.EXPECT().GenerateImage(int64(1), "firefox", "").Return("https", nil), + mapi.EXPECT().Edit(&tele.Message{ID: 1}, &tele.Photo{ + File: tele.FromURL("https"), + }).Return(nil, nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("first send ret err", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: "/image firefox"}).Times(1), + + mctx.EXPECT().Message().Return(&tele.Message{ID: 1}).Times(1), + mapi.EXPECT().Reply(&tele.Message{ID: 1}, "⚙️ Генерирую изображение ...").Return(&tele.Message{ID: 1}, errExp).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) + t.Run("GenerateImage send err", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), + mctx.EXPECT().Message().Return(&tele.Message{Text: "/image firefox"}).Times(1), + + mctx.EXPECT().Message().Return(&tele.Message{ID: 1}).Times(1), + mapi.EXPECT().Reply(&tele.Message{ID: 1}, "⚙️ Генерирую изображение ...").Return(&tele.Message{ID: 1}, nil).Times(1), + + gen.EXPECT().GenerateImage(int64(1), "firefox", "").Return("https", errExp), + mapi.EXPECT().Edit(&tele.Message{ID: 1}, "⚠️ К сожалению, я не смог сгенерировать изображение, попробуйте снова чуть позже").Return(&tele.Message{ID: 1}, nil).Times(1), + ) + + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} From 9bd0b92af4b5587943bd491cd3758c43dfd6a75e Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 7 Apr 2025 16:23:21 +0300 Subject: [PATCH 23/44] add handler refresh group + service --- internal/app/telegram/app.go | 6 +-- internal/services/{ => groups}/group.go | 17 +++++-- internal/services/groups/refreshGroups.go | 48 +++++++++++++++++++ .../handlers/callback/refreshGroups.go | 32 +++++++++++++ test/services/group_test.go | 4 +- 5 files changed, 97 insertions(+), 10 deletions(-) rename internal/services/{ => groups}/group.go (62%) create mode 100644 internal/services/groups/refreshGroups.go create mode 100644 internal/telegram/handlers/callback/refreshGroups.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 310bf1c..17a6236 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -6,7 +6,7 @@ import ( "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" "algobot/internal/lib/serdes/base62" - "algobot/internal/services" + "algobot/internal/services/groups" grpc2 "algobot/internal/services/grpc" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" @@ -33,7 +33,7 @@ type App struct { func New( log *slog.Logger, token string, - grGetter services.GroupGetter, + grGetter groups.GroupGetter, auther auth.Auther, set text.UserInformer, cookieSetter text.CookieSetter, @@ -66,7 +66,7 @@ func New( } // dependencies - groupServ := services.NewGroup(log, grGetter) + groupServ := groups.NewGroup(log, grGetter) stateMachine := memory.New() serdes := base62.NewSerdes(log) grpc := grpc2.NewAIService( diff --git a/internal/services/group.go b/internal/services/groups/group.go similarity index 62% rename from internal/services/group.go rename to internal/services/groups/group.go index fd67331..2ef22b2 100644 --- a/internal/services/group.go +++ b/internal/services/groups/group.go @@ -1,24 +1,31 @@ -package services +package groups import ( "algobot/internal/domain/models" "algobot/internal/lib/logger/sl" "algobot/internal/lib/sort" + "errors" "fmt" "log/slog" ) +var ( + ErrNotValidCookie = errors.New("not a valid cookie") +) + type GroupGetter interface { Groups(uid int64) ([]models.Group, error) } type Group struct { - log *slog.Logger - getter GroupGetter + log *slog.Logger + getter GroupGetter + groupFetcher GroupFetcher + domainSetter DomainSetter } -func NewGroup(log *slog.Logger, getter GroupGetter) *Group { - return &Group{log: log, getter: getter} +func NewGroup(log *slog.Logger, getter GroupGetter, setter DomainSetter, groupFetcher GroupFetcher) *Group { + return &Group{log: log, getter: getter, domainSetter: setter, groupFetcher: groupFetcher} } func (g *Group) Groups(uid int64, traceID interface{}) ([]models.Group, error) { diff --git a/internal/services/groups/refreshGroups.go b/internal/services/groups/refreshGroups.go new file mode 100644 index 0000000..f96d91f --- /dev/null +++ b/internal/services/groups/refreshGroups.go @@ -0,0 +1,48 @@ +package groups + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "log/slog" +) + +type DomainSetter interface { + SetGroups(uid int64, groups []models.Group) error + Cookies(uid int64) (string, error) +} + +type GroupFetcher interface { + Group(cookie string) ([]models.Group, error) +} + +func (g *Group) RefreshGroup(uid int64, traceID interface{}) error { + const op = "services.Group.RefreshGroup" + + log := g.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + cookie, err := g.domainSetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return fmt.Errorf("%s failed to get cookies: %w", op, err) + } + if cookie == "" { + return fmt.Errorf("%s cookie is empty: %w", op, ErrNotValidCookie) + } + + groups, err := g.groupFetcher.Group(cookie) + if err != nil { + log.Warn("failed to fetch groups", sl.Err(err)) + return fmt.Errorf("%s failed to fetch groups: %w", op, err) + } + + if err := g.domainSetter.SetGroups(uid, groups); err != nil { + log.Warn("failed to set groups", sl.Err(err)) + return fmt.Errorf("%s failed to set groups: %w", op, err) + } + + return nil +} diff --git a/internal/telegram/handlers/callback/refreshGroups.go b/internal/telegram/handlers/callback/refreshGroups.go new file mode 100644 index 0000000..9601dda --- /dev/null +++ b/internal/telegram/handlers/callback/refreshGroups.go @@ -0,0 +1,32 @@ +package callback + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" +) + +type GroupRefresher interface { + RefreshGroup(uid int64, traceID interface{}) error +} + +func RefreshGroup(refresher GroupRefresher, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewChangeNotification" + + traceID := ctx.Get("trace_id") + log := log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + uid := ctx.Sender().ID + + if err := refresher.RefreshGroup(uid, traceID); err != nil { + log.Warn("error while refreshing group", sl.Err(err)) + return fmt.Errorf("%s error while refreshing group: %w", op, err) + } + + return ctx.Edit("Успешно обновлено!") + } +} diff --git a/test/services/group_test.go b/test/services/group_test.go index a98eed0..e90f55d 100644 --- a/test/services/group_test.go +++ b/test/services/group_test.go @@ -2,7 +2,7 @@ package test import ( "algobot/internal/domain/models" - "algobot/internal/services" + "algobot/internal/services/groups" "algobot/test/mocks" mocks2 "algobot/test/mocks/services" "errors" @@ -19,7 +19,7 @@ func TestGroup(t *testing.T) { log := mocks.NewMockLogger() gGetter := mocks2.NewMockGroupGetter(ctrl) - service := services.NewGroup(log, gGetter) + service := groups.NewGroup(log, gGetter) t.Run("happy path", func(t *testing.T) { gGetter.EXPECT().Groups(int64(1)).Return(assets, nil).Times(1) From 56f273da454e2a0cd28dd6c442a47e6cd4f28048 Mon Sep 17 00:00:00 2001 From: pavlov Date: Tue, 8 Apr 2025 11:05:02 +0300 Subject: [PATCH 24/44] add tests for handler and service --- go.mod | 4 +- go.sum | 23 +++- test/mocks/services/mockgen.go | 6 +- .../mocks/telegram/handlers/groupRefresher.go | 54 ++++++++ test/mocks/telegram/handlers/mockgen.go | 2 + test/services/group_test.go | 116 ++++++++++++++---- test/telegram/handlers/refreshGroups_test.go | 44 +++++++ 7 files changed, 218 insertions(+), 31 deletions(-) create mode 100644 test/mocks/telegram/handlers/groupRefresher.go create mode 100644 test/telegram/handlers/refreshGroups_test.go diff --git a/go.mod b/go.mod index ccee369..e6a7334 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + google.golang.org/grpc v1.71.1 + google.golang.org/protobuf v1.36.6 gopkg.in/telebot.v4 v4.0.0-beta.4 ) @@ -30,8 +32,6 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 92aaf87..289a79a 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,6 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/LZTD1/telebot-context-router v1.0.1 h1:iGrKhFevnXFtQre+3bum6g9AluMTE23IVz2A+cOZPYE= -github.com/LZTD1/telebot-context-router v1.0.1/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= github.com/LZTD1/telebot-context-router v1.1.0 h1:WCdHvW+dcgGU0NBWIq3Cc6X++UeWQ4BzQ6tr3AdHxfI= github.com/LZTD1/telebot-context-router v1.1.0/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -133,6 +131,10 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -173,6 +175,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -190,6 +194,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -412,6 +418,18 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -831,7 +849,6 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index d6f0651..8b5b212 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -1,5 +1,7 @@ package mocks -//go:generate mockgen -destination=./groupGetter_mock.go -package=mocks algobot/internal/services GroupGetter - //go:generate mockgen -destination=./aiClient_mock.go -package=mocks algobot/protos AiClient + +//go:generate mockgen -destination=./groupGetter_mock.go -package=mocks algobot/internal/services/groups GroupGetter +//go:generate mockgen -destination=./domainSetter_mock.go -package=mocks algobot/internal/services/groups DomainSetter +//go:generate mockgen -destination=./groupFetcher_mock.go -package=mocks algobot/internal/services/groups GroupFetcher diff --git a/test/mocks/telegram/handlers/groupRefresher.go b/test/mocks/telegram/handlers/groupRefresher.go new file mode 100644 index 0000000..066e7d9 --- /dev/null +++ b/test/mocks/telegram/handlers/groupRefresher.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: algobot/internal/telegram/handlers/callback (interfaces: GroupRefresher) +// +// Generated by this command: +// +// mockgen -destination=./groupRefresher.go -package=mocks algobot/internal/telegram/handlers/callback GroupRefresher +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockGroupRefresher is a mock of GroupRefresher interface. +type MockGroupRefresher struct { + ctrl *gomock.Controller + recorder *MockGroupRefresherMockRecorder + isgomock struct{} +} + +// MockGroupRefresherMockRecorder is the mock recorder for MockGroupRefresher. +type MockGroupRefresherMockRecorder struct { + mock *MockGroupRefresher +} + +// NewMockGroupRefresher creates a new mock instance. +func NewMockGroupRefresher(ctrl *gomock.Controller) *MockGroupRefresher { + mock := &MockGroupRefresher{ctrl: ctrl} + mock.recorder = &MockGroupRefresherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGroupRefresher) EXPECT() *MockGroupRefresherMockRecorder { + return m.recorder +} + +// RefreshGroup mocks base method. +func (m *MockGroupRefresher) RefreshGroup(uid int64, traceID any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RefreshGroup", uid, traceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RefreshGroup indicates an expected call of RefreshGroup. +func (mr *MockGroupRefresherMockRecorder) RefreshGroup(uid, traceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshGroup", reflect.TypeOf((*MockGroupRefresher)(nil).RefreshGroup), uid, traceID) +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index cf57c14..7e07728 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -18,3 +18,5 @@ package mocks //go:generate mockgen -destination=./generatorImage_mock.go -package=mocks algobot/internal/telegram/handlers/text GeneratorImage //go:generate mockgen -destination=./chatter_mock.go -package=mocks algobot/internal/telegram/handlers/text Chatter + +//go:generate mockgen -destination=./groupRefresher.go -package=mocks algobot/internal/telegram/handlers/callback GroupRefresher diff --git a/test/services/group_test.go b/test/services/group_test.go index e90f55d..12f6269 100644 --- a/test/services/group_test.go +++ b/test/services/group_test.go @@ -18,38 +18,106 @@ func TestGroup(t *testing.T) { log := mocks.NewMockLogger() gGetter := mocks2.NewMockGroupGetter(ctrl) + setter := mocks2.NewMockDomainSetter(ctrl) + fetcher := mocks2.NewMockGroupFetcher(ctrl) - service := groups.NewGroup(log, gGetter) + service := groups.NewGroup( + log, + gGetter, + setter, + fetcher, + ) - t.Run("happy path", func(t *testing.T) { - gGetter.EXPECT().Groups(int64(1)).Return(assets, nil).Times(1) - gr, err := service.Groups(1, "trace_id") - assert.NoError(t, err) - assert.Equal(t, []models.Group{ + t.Run("Groups", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + gGetter.EXPECT().Groups(int64(1)).Return(assets, nil).Times(1) + gr, err := service.Groups(1, "trace_id") + assert.NoError(t, err) + assert.Equal(t, []models.Group{ + { + GroupID: 999, + Title: "group 3", + TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1001, + Title: "group 1", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + { + GroupID: 1000, + Title: "group 2", + TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + }, + }, gr) + }) + t.Run("Groups return err", func(t *testing.T) { + errExp := errors.New("some error") + gGetter.EXPECT().Groups(int64(1)).Return(nil, errExp).Times(1) + _, err := service.Groups(1, "trace_id") + assert.ErrorIs(t, err, errExp) + }) + }) + t.Run("RefreshGroup", func(t *testing.T) { + stubGroups := []models.Group{ { - GroupID: 999, - Title: "group 3", + GroupID: 1, + Title: "title1", TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), }, { - GroupID: 1001, - Title: "group 1", - TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), - }, - { - GroupID: 1000, - Title: "group 2", - TimeLesson: time.Date(2025, time.March, 23, 16, 0, 0, 0, time.UTC), + GroupID: 2, + Title: "title2", + TimeLesson: time.Date(2025, time.March, 22, 16, 0, 0, 0, time.UTC), }, - }, gr) - }) - t.Run("Groups return err", func(t *testing.T) { - errExp := errors.New("some error") - gGetter.EXPECT().Groups(int64(1)).Return(nil, errExp).Times(1) - _, err := service.Groups(1, "trace_id") - assert.ErrorIs(t, err, errExp) - }) + } + + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(int64(1)).Return("cookie", nil).Times(1), + fetcher.EXPECT().Group("cookie").Return(stubGroups, nil).Times(1), + setter.EXPECT().SetGroups(int64(1), stubGroups).Return(nil).Times(1), + ) + err := service.RefreshGroup(1, "trace_id") + assert.NoError(t, err) + }) + t.Run("Cookies return err", func(t *testing.T) { + errExp := errors.New("some error") + + setter.EXPECT().Cookies(int64(1)).Return("cookie", errExp).Times(1) + err := service.RefreshGroup(1, "trace_id") + assert.ErrorIs(t, err, errExp) + }) + t.Run("Cookies return empty cookie", func(t *testing.T) { + setter.EXPECT().Cookies(int64(1)).Return("", nil).Times(1) + err := service.RefreshGroup(1, "trace_id") + assert.ErrorIs(t, err, groups.ErrNotValidCookie) + }) + t.Run("fetcher Group return err", func(t *testing.T) { + errExp := errors.New("some error") + + gomock.InOrder( + setter.EXPECT().Cookies(int64(1)).Return("cookie", nil).Times(1), + fetcher.EXPECT().Group("cookie").Return(stubGroups, errExp).Times(1), + ) + + err := service.RefreshGroup(1, "trace_id") + assert.ErrorIs(t, err, errExp) + }) + t.Run("fetcher SetGroups return err", func(t *testing.T) { + errExp2 := errors.New("some error") + + gomock.InOrder( + setter.EXPECT().Cookies(int64(1)).Return("cookie", nil).Times(1), + fetcher.EXPECT().Group("cookie").Return(stubGroups, nil).Times(1), + setter.EXPECT().SetGroups(int64(1), stubGroups).Return(errExp2).Times(1), + ) + + err := service.RefreshGroup(1, "trace_id") + assert.ErrorIs(t, err, errExp2) + }) + }) } var assets = []models.Group{ diff --git a/test/telegram/handlers/refreshGroups_test.go b/test/telegram/handlers/refreshGroups_test.go new file mode 100644 index 0000000..5a04e96 --- /dev/null +++ b/test/telegram/handlers/refreshGroups_test.go @@ -0,0 +1,44 @@ +package test + +import ( + "algobot/internal/telegram/handlers/callback" + mocks2 "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestRefreshGroups(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + refresher := mocks.NewMockGroupRefresher(ctrl) + log := mocks2.NewMockLogger() + mctx := mocks3.NewMockContext(ctrl) + + handler := callback.RefreshGroup(refresher, log) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).AnyTimes() + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + refresher.EXPECT().RefreshGroup(int64(1), "").Return(nil), + mctx.EXPECT().Edit("Успешно обновлено!"), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("refresher return err", func(t *testing.T) { + errExp := errors.New("exp") + + gomock.InOrder( + refresher.EXPECT().RefreshGroup(int64(1), "").Return(errExp), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} From 7063f461355276ba4e5053dbfc3faa345bf68d70 Mon Sep 17 00:00:00 2001 From: pavlov Date: Tue, 8 Apr 2025 15:20:11 +0300 Subject: [PATCH 25/44] add refresh groups + tests --- config/dev.yaml | 6 +- go.mod | 2 + go.sum | 52 ++ internal/app/app.go | 5 + internal/app/telegram/app.go | 5 +- internal/config/config.go | 19 +- internal/lib/backoffice/backoffice.go | 110 +++ internal/lib/backoffice/backoffice_test.go | 117 +++ internal/lib/backoffice/group.go | 91 ++ internal/services/groups/group.go | 1 + internal/services/groups/refreshGroups.go | 4 + internal/storage/sqlite/setGroups.go | 45 + .../handlers/callback/refreshGroups.go | 5 + internal/telegram/handlers/text/myGroups.go | 2 +- internal/telegram/middleware/logger/logger.go | 16 +- test/lib/backoffice/backoffice_test.go | 61 ++ test/lib/backoffice/group_example | 858 ++++++++++++++++++ test/services/group_test.go | 9 + test/storage/sqlite_test.go | 21 + test/telegram/handlers/myGroups_test.go | 4 +- test/telegram/handlers/refreshGroups_test.go | 9 + 21 files changed, 1423 insertions(+), 19 deletions(-) create mode 100644 internal/lib/backoffice/backoffice.go create mode 100644 internal/lib/backoffice/backoffice_test.go create mode 100644 internal/lib/backoffice/group.go create mode 100644 internal/storage/sqlite/setGroups.go create mode 100644 test/lib/backoffice/backoffice_test.go create mode 100644 test/lib/backoffice/group_example diff --git a/config/dev.yaml b/config/dev.yaml index b80c9e8..94298e3 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -8,4 +8,8 @@ grpc: timeout: 300s rate_limit: fill_period: 800ms - bucket_limit: 6 \ No newline at end of file + bucket_limit: 6 +backoffice: + retries: 3 + retries_timeout: 5s + response_timeout: 15s \ No newline at end of file diff --git a/go.mod b/go.mod index e6a7334..0abd098 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect + github.com/PuerkitoBio/goquery v1.10.2 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect diff --git a/go.sum b/go.sum index 289a79a..03e7fec 100644 --- a/go.sum +++ b/go.sum @@ -63,11 +63,15 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/LZTD1/telebot-context-router v1.1.0 h1:WCdHvW+dcgGU0NBWIq3Cc6X++UeWQ4BzQ6tr3AdHxfI= github.com/LZTD1/telebot-context-router v1.1.0/go.mod h1:9A7AdlYAjrjNy6bnvB8MTl1UK8jNakfNAPKigTcgjU8= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8= +github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -407,6 +411,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= @@ -446,8 +451,13 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -485,6 +495,11 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -532,6 +547,13 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -566,6 +588,12 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -646,10 +674,24 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -659,6 +701,12 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -720,6 +768,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= diff --git a/internal/app/app.go b/internal/app/app.go index 4deb068..b447059 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,6 +3,7 @@ package app import ( "algobot/internal/app/telegram" "algobot/internal/config" + "algobot/internal/lib/backoffice" "algobot/internal/storage/sqlite" "log/slog" ) @@ -19,6 +20,8 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } + bo := backoffice.NewBackoffice(&cfg.Backoffice) + botApplication := telegram.New( log, cfg.TelegramToken, @@ -29,6 +32,8 @@ func New(log *slog.Logger, cfg *config.Config) *App { storage, cfg.RateLimit, cfg.GRPC, + storage, + bo, ) return &App{log: log, cfg: cfg, TelegramBot: botApplication} diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 17a6236..3e8b31c 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -40,6 +40,8 @@ func New( notifChanger callback.NotificationChanger, rateCfg config.RateLimit, grpcCfg config.GRPC, + dSetter groups.DomainSetter, + gFetcher groups.GroupFetcher, ) *App { const op = "telegram.New" @@ -66,7 +68,7 @@ func New( } // dependencies - groupServ := groups.NewGroup(log, grGetter) + groupServ := groups.NewGroup(log, grGetter, dSetter, gFetcher) stateMachine := memory.New() serdes := base62.NewSerdes(log) grpc := grpc2.NewAIService( @@ -96,6 +98,7 @@ func New( // callbacks r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(notifChanger, log)) + r.HandleFuncCallback("\frefresh_groups", callback.RefreshGroup(groupServ, log)) }) r.Group(func(r router.Router) { // Routes for SendingCookie state diff --git a/internal/config/config.go b/internal/config/config.go index e7c0d40..26460af 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,12 +8,19 @@ import ( ) type Config struct { - Env string `yaml:"env" env-default:"prod"` - TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` - MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` - StoragePath string `yaml:"storage_path" env-default:"./storage/storage.db"` - GRPC GRPC `yaml:"grpc"` - RateLimit RateLimit `yaml:"rate_limit"` + Env string `yaml:"env" env-default:"prod"` + TelegramToken string `yaml:"telegram_token" env:"TELEGRAM_TOKEN" env-required:"true"` + MigrationsPath string `yaml:"migrations_path" env-default:"./migrations"` + StoragePath string `yaml:"storage_path" env-default:"./storage/storage.db"` + GRPC GRPC `yaml:"grpc"` + RateLimit RateLimit `yaml:"rate_limit"` + Backoffice Backoffice `yaml:"backoffice"` +} + +type Backoffice struct { + Retries int `yaml:"retries" env-default:"3"` + RetriesTimeout time.Duration `yaml:"retries_timeout" env-default:"5s"` + ResponseTimeout time.Duration `yaml:"response_timeout" env-default:"15s"` } type RateLimit struct { diff --git a/internal/lib/backoffice/backoffice.go b/internal/lib/backoffice/backoffice.go new file mode 100644 index 0000000..64c8938 --- /dev/null +++ b/internal/lib/backoffice/backoffice.go @@ -0,0 +1,110 @@ +package backoffice + +import ( + "algobot/internal/config" + "algobot/internal/lib/logger/handlers/slogpretty" + "algobot/internal/lib/logger/sl" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "time" +) + +var ( + ErrBadCode = errors.New("bad code") +) + +type Option func(*Backoffice) + +type Backoffice struct { + url string + client *http.Client + log *slog.Logger + cfg *config.Backoffice +} + +func NewBackoffice(cfg *config.Backoffice, fn ...Option) *Backoffice { + bo := &Backoffice{ + cfg: cfg, + url: "https://backoffice.algoritmika.org", + client: &http.Client{ + Timeout: cfg.ResponseTimeout, + }, + log: slog.New(slogpretty.NewHandler(&slog.HandlerOptions{Level: slog.LevelDebug})), + } + for _, o := range fn { + o(bo) + } + + return bo +} + +func WithURL(url string) func(*Backoffice) { + return func(bo *Backoffice) { + bo.url = url + } +} +func WithLogger(log *slog.Logger) func(*Backoffice) { + return func(bo *Backoffice) { + bo.log = log + } +} + +func (bo *Backoffice) doReq(req *http.Request) (*http.Response, error) { + const op = "backoffice.doReq" + log := bo.log.With( + slog.String("op", op), + ) + + var err error + var resp *http.Response + + for i := 0; i < bo.cfg.Retries; i++ { + resp, err = bo.client.Do(req) + if err != nil { + log.Debug("error while req bo", sl.Err(err)) + time.Sleep(bo.cfg.RetriesTimeout) + continue + } + if resp.StatusCode != http.StatusOK { + log.Debug("received not 200 OK", slog.Int("status", resp.StatusCode)) + err = ErrBadCode + time.Sleep(bo.cfg.RetriesTimeout) + continue + } + return resp, nil + } + + if err != nil { + log.Warn("error while req bo", sl.Err(err)) + return nil, fmt.Errorf("%s error while trying send request: %w", op, err) + } + + return resp, nil +} +func (bo *Backoffice) createReq(method, uri, cookie string, params map[string]string, body io.Reader) (*http.Request, error) { + const op = "backoffice.createReq" + + reqUrl, _ := url.Parse(fmt.Sprintf("%s%s", bo.url, uri)) + + p := url.Values{} + for key, val := range params { + p.Add(key, val) + } + reqUrl.RawQuery = p.Encode() + + req, err := http.NewRequest(method, reqUrl.String(), body) + if err != nil { + return nil, fmt.Errorf("%s error while creating req: %w", op, err) + } + + req.Header.Add("Cookie", cookie) + if method == "POST" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + } + + return req, nil +} diff --git a/internal/lib/backoffice/backoffice_test.go b/internal/lib/backoffice/backoffice_test.go new file mode 100644 index 0000000..5e10585 --- /dev/null +++ b/internal/lib/backoffice/backoffice_test.go @@ -0,0 +1,117 @@ +package backoffice + +import ( + "algobot/internal/config" + "algobot/test/mocks" + "context" + "github.com/stretchr/testify/assert" + "math/rand" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestRequester(t *testing.T) { + t.Run("test timeout", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + time.Sleep(1 * time.Second) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := NewBackoffice(&config.Backoffice{ + Retries: 1, + RetriesTimeout: time.Millisecond, + ResponseTimeout: 10 * time.Millisecond, + }, WithLogger(mocks.NewMockLogger()), WithURL(server.URL)) + + req, _ := http.NewRequest(http.MethodGet, server.URL, nil) + _, err := bo.doReq(req) + + assert.Error(t, err) + assert.ErrorIs(t, err, context.DeadlineExceeded) + }) + t.Run("test retry not 200 OK", func(t *testing.T) { + counter := 0 + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + counter++ + rw.WriteHeader(rand.Intn(200) + 300) + })) + defer server.Close() + + bo := NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Millisecond, + ResponseTimeout: 100 * time.Millisecond, + }, WithLogger(mocks.NewMockLogger()), WithURL(server.URL)) + + req, _ := http.NewRequest(http.MethodGet, server.URL, nil) + _, err := bo.doReq(req) + + assert.Error(t, err) + assert.Equal(t, 5, counter) + assert.ErrorIs(t, err, ErrBadCode) + }) + t.Run("test retries count", func(t *testing.T) { + counter := 0 + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + counter++ + time.Sleep(1 * time.Second) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Millisecond, + ResponseTimeout: 10 * time.Millisecond, + }, WithLogger(mocks.NewMockLogger()), WithURL(server.URL)) + + req, _ := http.NewRequest(http.MethodGet, server.URL, nil) + _, err := bo.doReq(req) + + assert.Error(t, err) + assert.Equal(t, 5, counter) + assert.ErrorIs(t, err, context.DeadlineExceeded) + }) + t.Run("test retries timeout", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + time.Sleep(1 * time.Second) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: 5 * time.Millisecond, + ResponseTimeout: 10 * time.Millisecond, + }, WithLogger(mocks.NewMockLogger()), WithURL(server.URL)) + + req, _ := http.NewRequest(http.MethodGet, server.URL, nil) + + timeStart := time.Now() + _, err := bo.doReq(req) + timeEnd := time.Now() + + assert.Error(t, err) + assert.InDelta(t, 75, timeEnd.Sub(timeStart).Milliseconds(), 5) + }) + t.Run("happy path", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: 50 * time.Millisecond, + ResponseTimeout: 100 * time.Millisecond, + }, WithLogger(mocks.NewMockLogger()), WithURL(server.URL)) + req, _ := http.NewRequest(http.MethodGet, server.URL, nil) + + resp, err := bo.doReq(req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + }) +} diff --git a/internal/lib/backoffice/group.go b/internal/lib/backoffice/group.go new file mode 100644 index 0000000..03e41b6 --- /dev/null +++ b/internal/lib/backoffice/group.go @@ -0,0 +1,91 @@ +package backoffice + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "github.com/PuerkitoBio/goquery" + "io" + "log/slog" + "strconv" + "strings" + "time" +) + +func (bo *Backoffice) Group(cookie string) ([]models.Group, error) { + const op = "backoffice.Group" + log := bo.log.With( + slog.String("op", op), + ) + + req, err := bo.createReq("GET", "/group", cookie, map[string]string{ + "GroupSearch[status][]": "active", + "presetType": "all", + "_pjax": "#group-grid-pjax", + }, nil) + + if err != nil { + log.Warn("failed to create request", sl.Err(err)) + return nil, fmt.Errorf("%s failed to create request: %w", op, err) + } + data, err := bo.doReq(req) + if err != nil { + return nil, fmt.Errorf("%s failed to doReq: %w", op, err) + } + + res, err := parseHTML(data.Body, bo.log) + if err != nil { + return nil, fmt.Errorf("%s failed to parse HTML: %w", op, err) + } + + return res, nil +} + +func parseHTML(body io.ReadCloser, log *slog.Logger) ([]models.Group, error) { + const op = "backoffice.parseHTML" + log = log.With(slog.String("op", op)) + + doc, err := goquery.NewDocumentFromReader(body) + if err != nil { + return nil, fmt.Errorf("%s failed parse doc: %w", op, err) + } + + var groups []models.Group + + doc.Find("tr.group-grid").Each(func(i int, row *goquery.Selection) { + groupId := row.Find("td[data-col-seq='id']").First().Text() + groupId = strings.TrimSpace(groupId) + + titleCell := row.Find("td[data-col-seq='title']").First() + + groupTitle := titleCell.Find("p").First().Text() + groupTitle = strings.TrimSpace(groupTitle) + + groupTime := titleCell.Find("a").First().Text() + groupTime = strings.TrimSpace(groupTime) + + nextLessonTime := row.Find("td[data-col-seq='nextLessonTime']").First().Text() + nextLessonTime = strings.TrimSpace(nextLessonTime) + + if nextLessonTime != "" { + groupID, err := strconv.Atoi(strings.ReplaceAll(groupId, "\u00A0", " ")) + if err != nil { + log.Warn("failed to convert group id to int", sl.Err(err)) + return + } + timeLession, err := time.Parse("02.01.2006 15:04", strings.ReplaceAll(nextLessonTime, "\u00A0", " ")) + if err != nil { + log.Warn("failed to convert timeLession to time", sl.Err(err)) + return + } + + groups = append(groups, models.Group{ + GroupID: groupID, + Title: strings.ReplaceAll(groupTitle, "\u00A0", " "), + TimeLesson: timeLession, + }) + } + }) + + return groups, nil +} diff --git a/internal/services/groups/group.go b/internal/services/groups/group.go index 2ef22b2..99aa256 100644 --- a/internal/services/groups/group.go +++ b/internal/services/groups/group.go @@ -11,6 +11,7 @@ import ( var ( ErrNotValidCookie = errors.New("not a valid cookie") + ErrNoGroups = errors.New("groups not found") ) type GroupGetter interface { diff --git a/internal/services/groups/refreshGroups.go b/internal/services/groups/refreshGroups.go index f96d91f..75b5a0e 100644 --- a/internal/services/groups/refreshGroups.go +++ b/internal/services/groups/refreshGroups.go @@ -39,6 +39,10 @@ func (g *Group) RefreshGroup(uid int64, traceID interface{}) error { return fmt.Errorf("%s failed to fetch groups: %w", op, err) } + if len(groups) == 0 { + return fmt.Errorf("%s no groups found: %w", op, ErrNoGroups) + } + if err := g.domainSetter.SetGroups(uid, groups); err != nil { log.Warn("failed to set groups", sl.Err(err)) return fmt.Errorf("%s failed to set groups: %w", op, err) diff --git a/internal/storage/sqlite/setGroups.go b/internal/storage/sqlite/setGroups.go new file mode 100644 index 0000000..cecba3b --- /dev/null +++ b/internal/storage/sqlite/setGroups.go @@ -0,0 +1,45 @@ +package sqlite + +import ( + "algobot/internal/domain/models" + "fmt" +) + +func (s *Sqlite) SetGroups(uid int64, groups []models.Group) error { + const op = "sqlite.SetGroups" + + // Drop all + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("%s error while start tx: %w", op, err) + } + pr, err := tx.Prepare(`DELETE FROM groups WHERE owner_id = ?;`) + if err != nil { + return fmt.Errorf("%s error while preparing sql: %w", op, err) + } + _, err = pr.Exec(uid) + if err != nil { + tx.Rollback() + return fmt.Errorf("%s error while exec sql: %w", op, err) + } + // Set new + pr, err = tx.Prepare(`INSERT INTO groups (group_id, owner_id, title, time_lesson) VALUES (?, ?, ?, ?);`) + if err != nil { + return fmt.Errorf("%s error while preparing groups sql: %w", op, err) + } + for _, group := range groups { + _, err = pr.Exec(group.GroupID, uid, group.Title, group.TimeLesson.Format("2006-01-02 15:04:05")) + if err != nil { + tx.Rollback() + return fmt.Errorf("%s error while exec add group sql: %w", op, err) + } + } + + err = tx.Commit() + if err != nil { + tx.Rollback() + return fmt.Errorf("%s error while commit tx: %w", op, err) + } + + return nil +} diff --git a/internal/telegram/handlers/callback/refreshGroups.go b/internal/telegram/handlers/callback/refreshGroups.go index 9601dda..3c9a24a 100644 --- a/internal/telegram/handlers/callback/refreshGroups.go +++ b/internal/telegram/handlers/callback/refreshGroups.go @@ -2,6 +2,8 @@ package callback import ( "algobot/internal/lib/logger/sl" + "algobot/internal/services/groups" + "errors" "fmt" "gopkg.in/telebot.v4" "log/slog" @@ -23,6 +25,9 @@ func RefreshGroup(refresher GroupRefresher, log *slog.Logger) telebot.HandlerFun uid := ctx.Sender().ID if err := refresher.RefreshGroup(uid, traceID); err != nil { + if errors.Is(err, groups.ErrNoGroups) { + return ctx.Edit("У вас не нашлось ни 1 группы!\nПроверьте ваши cookie") + } log.Warn("error while refreshing group", sl.Err(err)) return fmt.Errorf("%s error while refreshing group: %w", op, err) } diff --git a/internal/telegram/handlers/text/myGroups.go b/internal/telegram/handlers/text/myGroups.go index 5cf93ca..b673809 100644 --- a/internal/telegram/handlers/text/myGroups.go +++ b/internal/telegram/handlers/text/myGroups.go @@ -58,7 +58,7 @@ func (g *MyGroup) ServeContext(ctx telebot.Context) error { return ctx.Send(fmt.Sprintf("[%s] Ошибка при получении групп!", ctx.Get("trace_id")), telebot.ModeHTML) } - return ctx.Send(g.msgMyGroups(groups, ctx), telebot.ModeHTML, keyboards.RefreshGroups()) + return ctx.Send(g.msgMyGroups(groups, ctx), telebot.ModeMarkdown, keyboards.RefreshGroups()) } func (g *MyGroup) msgMyGroups(groups []models.Group, ctx telebot.Context) string { diff --git a/internal/telegram/middleware/logger/logger.go b/internal/telegram/middleware/logger/logger.go index dcb423e..32097e4 100644 --- a/internal/telegram/middleware/logger/logger.go +++ b/internal/telegram/middleware/logger/logger.go @@ -15,21 +15,21 @@ func New(log *slog.Logger) tele.MiddlewareFunc { return func(c tele.Context) error { traceID := c.Get("trace_id") - if msg := c.Message(); msg != nil { - log.Info("incoming message", + if cb := c.Callback(); cb != nil { + log.Info("incoming callback", slog.Int64("from_id", c.Sender().ID), slog.String("from", c.Sender().Username), slog.String("full_name", fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName)), - slog.String("message", msg.Text), - slog.Any("trace_id", traceID), + slog.String("message", cb.Data), ) - } - if cb := c.Callback(); cb != nil { - log.Info("incoming callback", + } else { + msg := c.Message() + log.Info("incoming message", slog.Int64("from_id", c.Sender().ID), slog.String("from", c.Sender().Username), slog.String("full_name", fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName)), - slog.String("message", cb.Data), + slog.String("message", msg.Text), + slog.Any("trace_id", traceID), ) } diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go new file mode 100644 index 0000000..ed95df7 --- /dev/null +++ b/test/lib/backoffice/backoffice_test.go @@ -0,0 +1,61 @@ +package backoffice + +import ( + "algobot/internal/config" + "algobot/internal/domain/models" + "algobot/internal/lib/backoffice" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +func TestBackoffice(t *testing.T) { + t.Run("Group", func(t *testing.T) { + groupsExp := []models.Group{ + {GroupID: 98637162, Title: "Группа по курсу КГ", TimeLesson: time.Date(2025, time.April, 13, 14, 0, 0, 0, time.UTC)}, + {GroupID: 98623404, Title: "Группа по курсу ОЛиП МП", TimeLesson: time.Date(2025, time.April, 13, 12, 0, 0, 0, time.UTC)}, + {GroupID: 98621252, Title: "Группа по курсу Пст", TimeLesson: time.Date(2025, time.April, 13, 18, 0, 0, 0, time.UTC)}, + {GroupID: 98619913, Title: "Группа по курсу КГ", TimeLesson: time.Date(2025, time.April, 12, 10, 0, 0, 0, time.UTC)}, + {GroupID: 98619873, Title: "Группа по курсу ГД", TimeLesson: time.Date(2025, time.April, 12, 14, 0, 0, 0, time.UTC)}, + {GroupID: 98619867, Title: "Группа по курсу ВП", TimeLesson: time.Date(2025, time.April, 12, 12, 0, 0, 0, time.UTC)}, + {GroupID: 98589447, Title: "Группа по курсу ВП", TimeLesson: time.Date(2025, time.April, 13, 10, 0, 0, 0, time.UTC)}, + {GroupID: 985504, Title: "Группа по курсу Пст 2", TimeLesson: time.Date(2025, time.April, 12, 18, 0, 0, 0, time.UTC)}, + {GroupID: 978298, Title: "Группа по курсу Пст 2", TimeLesson: time.Date(2025, time.April, 13, 16, 0, 0, 0, time.UTC)}, + } + + responseHTML := readFile("group_example") + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + group, err := bo.Group("cookie") + assert.NoError(t, err) + assert.Equal(t, groupsExp, group) + }) +} + +func readFile(fileName string) string { + file, err := os.Open(fileName) + if err != nil { + panic(err) + } + b, err := io.ReadAll(file) + if err != nil { + panic(err) + } + responseHTML := string(b) + return responseHTML +} diff --git a/test/lib/backoffice/group_example b/test/lib/backoffice/group_example new file mode 100644 index 0000000..9e8f19f --- /dev/null +++ b/test/lib/backoffice/group_example @@ -0,0 +1,858 @@ + + + + + + +
+ + + + + +
+
+
+ +
+
+
+
+ +
+
+

Группы

+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + +
+ +
+ + + Записей на странице +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID + НазваниеПлощадкаУч-ки ЗачисленныеВремя след. урока След. урок + След. урокПреподавательКураторТип группыСтатусФормат
+   + + + +
  +
+
+
+   +
+
+
+   +
+
98637162Библиотека 5 вс + 14.00 +

Группа по курсу КГ

Библиотека №58 (0) + 0 + 13.04.2025 14:0029Понятие о деньгах и + бюджете +
КГ М8У1
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98624852Библиотека 5 вс + 12.00 +

МК по ОЛиП

Библиотека №53 (0) + 4 + Данил ДаниловМаксим МаксимовМастер-классИдет набор +
Офлайн
+
98623404Библиотека 5 вс + 12.00 +

Группа по курсу ОЛиП МП

Библиотека №58 (0) + 0 + 13.04.2025 12:0031Урок 28.Презентация + проектов +
М7У4, курс "Основы логики и программирования", + 2021-2022 +
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98621252Библиотека 5 вс + 18.00 +

Группа по курсу Пст

Библиотека №59 (0) + 0 + 13.04.2025 18:0030М6 У5. Игра Fast + Clicker. Ч. 3 +
Python Start 2021/2022 М6 У5
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98621166Библиотека 5 вс + 14.00 +

Мастер-класс по курсу КГ

Библиотека №53 (0) + 4 + Данил ДаниловМаксим МаксимовМастер-классИдет набор +
Офлайн
+
98619913Библиотека № 7 сб + 10.00 +

Группа по курсу КГ

Библиотека №58 (0) + 0 + 12.04.2025 10:0029Понятие о деньгах и + бюджете +
КГ М8У1
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98619873Библиотека № 7 сб + 14.00 +

Группа по курсу ГД

Библиотека №58 (0) + 0 + 12.04.2025 14:0029Представляем + симулятор гонок +
ГД 21/22 М6У4
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98619867Библиотека № 7 сб + 12.00 +

Группа по курсу ВП

Библиотека №58 (0) + 0 + 12.04.2025 12:0028М5У6. Финализация и + презентация проекта +
Визуальное программирование, М5У6
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
98589447Библиотека № 7 вс + 10.00 +

Группа по курсу ВП

Библиотека №56 (0) + 0 + 13.04.2025 10:0031М6У1. Классы и + объекты +
Визуальное программирование, М6У1
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
985504Библиотека 5 сб + 18.00 +

Группа по курсу Пст 2

Библиотека №56 (0) + 0 + 12.04.2025 18:0031М6 У1. Сборка + проекта в приложение +
Python Start - 2 2021/2022 М6 У1
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
978298Библиотека 5 вс + 16.00 +

Группа по курсу Пст 2

Библиотека №57 (0) + 0 + 13.04.2025 16:0031М5 У9. Доработка и + презентация проекта +
Python Start - 2 2021/2022 М5 У9
+
Данил ДаниловМаксим МаксимовГруппаАктивная +
Офлайн
+
+
+
+
+
+
    +
  • 1 +
  • +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/test/services/group_test.go b/test/services/group_test.go index 12f6269..b22651d 100644 --- a/test/services/group_test.go +++ b/test/services/group_test.go @@ -82,6 +82,15 @@ func TestGroup(t *testing.T) { err := service.RefreshGroup(1, "trace_id") assert.NoError(t, err) }) + t.Run("return empty group", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(int64(1)).Return("cookie", nil).Times(1), + fetcher.EXPECT().Group("cookie").Return([]models.Group{}, nil).Times(1), + ) + + err := service.RefreshGroup(1, "trace_id") + assert.ErrorIs(t, err, groups.ErrNoGroups) + }) t.Run("Cookies return err", func(t *testing.T) { errExp := errors.New("some error") diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index 6d1016f..a6e093d 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -146,4 +146,25 @@ func TestSqlite(t *testing.T) { assert.Len(t, groups, 0) }) }) + t.Run("SetGroups", func(t *testing.T) { + gr := []models.Group{ + { + GroupID: 1, + Title: "title", + TimeLesson: time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC), + }, + } + + groups, err := sqlite.Groups(999) + assert.NoError(t, err) + assert.Len(t, groups, 3) + + err = sqlite.SetGroups(999, gr) + assert.NoError(t, err) + + groups, err = sqlite.Groups(999) + assert.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, gr, groups) + }) } diff --git a/test/telegram/handlers/myGroups_test.go b/test/telegram/handlers/myGroups_test.go index c9f0cf6..2cf8426 100644 --- a/test/telegram/handlers/myGroups_test.go +++ b/test/telegram/handlers/myGroups_test.go @@ -36,7 +36,7 @@ func TestMyGroups(t *testing.T) { ser.EXPECT().Serialize(mockGroups[0], "trace_id").Return("ser-g1", nil).Times(1), ser.EXPECT().Serialize(mockGroups[1], "trace_id").Return("ser-g2", nil).Times(1), ser.EXPECT().Serialize(mockGroups[2], "trace_id").Return("", errors.New("ser")).Times(1), - mctx.EXPECT().Send(mockStringRet, tele.ModeHTML, keyboards.RefreshGroups()).Return(nil).Times(1), + mctx.EXPECT().Send(mockStringRet, tele.ModeMarkdown, keyboards.RefreshGroups()).Return(nil).Times(1), ) err := handler.ServeContext(mctx) @@ -46,7 +46,7 @@ func TestMyGroups(t *testing.T) { gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), grouper.EXPECT().Groups(int64(1), "trace_id").Return([]models.Group{}, nil).Times(1), - mctx.EXPECT().Send("Всего групп: 0\nПопробуйте обновить группы!", tele.ModeHTML, keyboards.RefreshGroups()).Return(nil).Times(1), + mctx.EXPECT().Send("Всего групп: 0\nПопробуйте обновить группы!", tele.ModeMarkdown, keyboards.RefreshGroups()).Return(nil).Times(1), ) err := handler.ServeContext(mctx) diff --git a/test/telegram/handlers/refreshGroups_test.go b/test/telegram/handlers/refreshGroups_test.go index 5a04e96..c03c451 100644 --- a/test/telegram/handlers/refreshGroups_test.go +++ b/test/telegram/handlers/refreshGroups_test.go @@ -1,6 +1,7 @@ package test import ( + "algobot/internal/services/groups" "algobot/internal/telegram/handlers/callback" mocks2 "algobot/test/mocks" mocks3 "algobot/test/mocks/telegram" @@ -32,6 +33,14 @@ func TestRefreshGroups(t *testing.T) { err := handler(mctx) assert.NoError(t, err) }) + t.Run("nop groups found", func(t *testing.T) { + gomock.InOrder( + refresher.EXPECT().RefreshGroup(int64(1), "").Return(groups.ErrNoGroups), + mctx.EXPECT().Edit("У вас не нашлось ни 1 группы!\nПроверьте ваши cookie"), + ) + err := handler(mctx) + assert.NoError(t, err) + }) t.Run("refresher return err", func(t *testing.T) { errExp := errors.New("exp") From a061f6ab82af3b86c9bcdad28fb3b08a38a0d7df Mon Sep 17 00:00:00 2001 From: pavlov Date: Tue, 8 Apr 2025 16:55:34 +0300 Subject: [PATCH 26/44] add start with payload --- internal/app/telegram/app.go | 2 + internal/domain/models/fullGroupInfo.go | 25 +++++++++ internal/lib/serdes/base62/base62.go | 25 +++++++++ internal/lib/serdes/group.go | 6 +++ .../telegram/handlers/text/viewInformer.go | 52 +++++++++++++++++++ 5 files changed, 110 insertions(+) create mode 100644 internal/domain/models/fullGroupInfo.go create mode 100644 internal/telegram/handlers/text/viewInformer.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 3e8b31c..cae6499 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -95,6 +95,8 @@ func New( r.HandleFuncText("AI 🔹", text.NewAI(grpc, log, stateMachine)) r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) + r.HandleFuncRegexpText(regexp.MustCompile(`^(?m)\/start\s(.+)$`), text.NewSettings(set, log)) + // callbacks r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(notifChanger, log)) diff --git a/internal/domain/models/fullGroupInfo.go b/internal/domain/models/fullGroupInfo.go new file mode 100644 index 0000000..7cb27f3 --- /dev/null +++ b/internal/domain/models/fullGroupInfo.go @@ -0,0 +1,25 @@ +package models + +import "time" + +type GroupView struct { + GroupID int + GroupTitle string + GroupContent string + NextLessonTime string + LessonsTotal int + LessonsPassed int + ActiveKids []GroupKid + NotActiveKids []GroupKid +} + +type GroupKid struct { + ID int + FullName string + LastGroup KidGroup +} +type KidGroup struct { + ID int + StartTime time.Time + EndTime time.Time +} diff --git a/internal/lib/serdes/base62/base62.go b/internal/lib/serdes/base62/base62.go index 4d8964f..24124be 100644 --- a/internal/lib/serdes/base62/base62.go +++ b/internal/lib/serdes/base62/base62.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/jxskiss/base62" "log/slog" + "strings" ) type Serdes struct { @@ -24,3 +25,27 @@ func (s *Serdes) Serialize(group models.Group, traceID interface{}) (string, err ))) return encoded, nil } + +func (s *Serdes) GetType(decoded string) (serdes.SerType, error) { + const op = "serdes.GetType" + log := s.log.With( + slog.String("op", op), + ) + + encoded, err := base62.DecodeString(decoded) + if err != nil { + log.Warn("Failed to decode serdes") + return 0, fmt.Errorf("%s: %w", op, err) + } + + encodedType := strings.Split(string(encoded), "-")[0] + + switch encodedType { + case "0": + return serdes.GroupType, nil + case "1": + return serdes.UserType, nil + default: + return 0, fmt.Errorf("%s is not a recognized serdes : %w", op, serdes.ErrUnrecognized) + } +} diff --git a/internal/lib/serdes/group.go b/internal/lib/serdes/group.go index 26fe357..0805136 100644 --- a/internal/lib/serdes/group.go +++ b/internal/lib/serdes/group.go @@ -1,5 +1,11 @@ package serdes +import "errors" + +var ( + ErrUnrecognized = errors.New("unrecognized sertype") +) + type SerType int const ( diff --git a/internal/telegram/handlers/text/viewInformer.go b/internal/telegram/handlers/text/viewInformer.go new file mode 100644 index 0000000..b9a2d44 --- /dev/null +++ b/internal/telegram/handlers/text/viewInformer.go @@ -0,0 +1,52 @@ +package text + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "algobot/internal/lib/serdes" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type FetchView interface { + GetGroupView(uid int64, groupID int) models.GroupView +} + +type Serializator interface { + GetType(encoded string) (serdes.SerType, error) +} + +func ViewInformer(ser Serializator, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.GenerateImage" + + uid := ctx.Sender().ID + traceID := ctx.Get("trace_id") + data := getData(ctx.Message().Text) + + log = log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + serType, err := ser.GetType(data) + if err != nil { + log.Warn("can't get ser type", sl.Err(err)) + return ctx.Send("⚠️ Ошибка при расшифровке запроса!") + } + + switch serType { + case serdes.UserType: + return nil + case serdes.GroupType: + return nil + default: + return ctx.Send("⚠️ Не удалось определить обработчик") + } + } +} + +func getData(text string) string { + return strings.TrimSpace(strings.TrimLeft(text, "/start")) +} From 6ee03b459db185f00961661a6d9672da76311a3c Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 11:36:14 +0300 Subject: [PATCH 27/44] add test for viewInformer.go handler --- internal/domain/models/kidView.go | 32 ++ internal/domain/serialize.go | 13 + internal/lib/serdes/base62/base62.go | 39 +-- internal/lib/serdes/group.go | 7 - .../telegram/handlers/text/viewInformer.go | 182 ++++++++++-- .../mocks/telegram/handlers/groupRefresher.go | 54 ---- test/mocks/telegram/handlers/mockgen.go | 5 +- test/telegram/handlers/viewInformer_test.go | 281 ++++++++++++++++++ 8 files changed, 510 insertions(+), 103 deletions(-) create mode 100644 internal/domain/models/kidView.go create mode 100644 internal/domain/serialize.go delete mode 100644 test/mocks/telegram/handlers/groupRefresher.go create mode 100644 test/telegram/handlers/viewInformer_test.go diff --git a/internal/domain/models/kidView.go b/internal/domain/models/kidView.go new file mode 100644 index 0000000..e92ff42 --- /dev/null +++ b/internal/domain/models/kidView.go @@ -0,0 +1,32 @@ +package models + +import "time" + +type ExtraInfo string + +var NotAccessible ExtraInfo = "not_accessible" + +type KidView struct { + Extra ExtraInfo + Kid Kid +} +type Kid struct { + FullName string `json:"fullName"` + ParentName string `json:"parentName"` + Email string `json:"email"` + Phone string `json:"phone"` + Age int `json:"age"` + BirthDate time.Time `json:"birthDate"` + Username string `json:"username"` + Password string `json:"password"` + Groups []KidViewGroup `json:"groups"` +} + +type KidViewGroup struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Status int `json:"status"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} diff --git a/internal/domain/serialize.go b/internal/domain/serialize.go new file mode 100644 index 0000000..da22ca9 --- /dev/null +++ b/internal/domain/serialize.go @@ -0,0 +1,13 @@ +package domain + +type SerType int + +const ( + GroupType SerType = iota + UserType +) + +type SerializeMessage struct { + Type SerType + Data []string +} diff --git a/internal/lib/serdes/base62/base62.go b/internal/lib/serdes/base62/base62.go index 24124be..d9893c7 100644 --- a/internal/lib/serdes/base62/base62.go +++ b/internal/lib/serdes/base62/base62.go @@ -1,11 +1,11 @@ package base62 import ( - "algobot/internal/domain/models" - "algobot/internal/lib/serdes" + "algobot/internal/domain" "fmt" "github.com/jxskiss/base62" "log/slog" + "strconv" "strings" ) @@ -17,17 +17,17 @@ func NewSerdes(log *slog.Logger) *Serdes { return &Serdes{log: log} } -func (s *Serdes) Serialize(group models.Group, traceID interface{}) (string, error) { +func (s *Serdes) Serialize(msg domain.SerializeMessage) (string, error) { encoded := base62.EncodeToString([]byte(fmt.Sprintf( - "%d-%d", - serdes.GroupType, - group.GroupID, + "%d-%s", + msg.Type, + strings.Join(msg.Data, ","), ))) return encoded, nil } -func (s *Serdes) GetType(decoded string) (serdes.SerType, error) { - const op = "serdes.GetType" +func (s *Serdes) Deserialize(decoded string) (*domain.SerializeMessage, error) { + const op = "serdes.Deserialize" log := s.log.With( slog.String("op", op), ) @@ -35,17 +35,22 @@ func (s *Serdes) GetType(decoded string) (serdes.SerType, error) { encoded, err := base62.DecodeString(decoded) if err != nil { log.Warn("Failed to decode serdes") - return 0, fmt.Errorf("%s: %w", op, err) + return nil, fmt.Errorf("%s: %w", op, err) } - encodedType := strings.Split(string(encoded), "-")[0] + encodedMsg := strings.Split(string(encoded), "-") + encodedType := encodedMsg[0] + encodedData := strings.Split(encodedMsg[1], ",") - switch encodedType { - case "0": - return serdes.GroupType, nil - case "1": - return serdes.UserType, nil - default: - return 0, fmt.Errorf("%s is not a recognized serdes : %w", op, serdes.ErrUnrecognized) + encodedID, err := strconv.Atoi(encodedType) + if err != nil { + log.Warn("Failed to decode serdes") + return nil, fmt.Errorf("%s: %w", op, err) } + serType := domain.SerType(encodedID) + + return &domain.SerializeMessage{ + Type: serType, + Data: encodedData, + }, nil } diff --git a/internal/lib/serdes/group.go b/internal/lib/serdes/group.go index 0805136..62370c3 100644 --- a/internal/lib/serdes/group.go +++ b/internal/lib/serdes/group.go @@ -5,10 +5,3 @@ import "errors" var ( ErrUnrecognized = errors.New("unrecognized sertype") ) - -type SerType int - -const ( - GroupType SerType = iota - UserType -) diff --git a/internal/telegram/handlers/text/viewInformer.go b/internal/telegram/handlers/text/viewInformer.go index b9a2d44..df70c24 100644 --- a/internal/telegram/handlers/text/viewInformer.go +++ b/internal/telegram/handlers/text/viewInformer.go @@ -1,50 +1,184 @@ package text import ( + "algobot/internal/domain" "algobot/internal/domain/models" "algobot/internal/lib/logger/sl" - "algobot/internal/lib/serdes" + "fmt" "gopkg.in/telebot.v4" "log/slog" + "regexp" + "strconv" "strings" ) -type FetchView interface { - GetGroupView(uid int64, groupID int) models.GroupView +var statuses = map[int]string{ + 0: "🟢 Учится", + 20: "🔴 Выбыл", + 10: "🟡 Переведен", +} + +type ViewFetcher interface { + GroupView(uid int64, groupID string) (models.GroupView, error) + KidView(uid int64, kidID string, groupId string) (models.KidView, error) } type Serializator interface { - GetType(encoded string) (serdes.SerType, error) + Serialize(msg domain.SerializeMessage) (string, error) + Deserialize(decoded string) (*domain.SerializeMessage, error) +} + +type ViewInformer struct { + serdes Serializator + viewFetcher ViewFetcher + log *slog.Logger + botName string +} + +func NewViewInformer(serdes Serializator, viewFetcher ViewFetcher, log *slog.Logger, botName string) *ViewInformer { + return &ViewInformer{serdes: serdes, viewFetcher: viewFetcher, log: log, botName: botName} +} + +func (v *ViewInformer) ServeContext(ctx telebot.Context) error { + const op = "text.GenerateImage" + + uid := ctx.Sender().ID + traceID := ctx.Get("trace_id") + data := getData(ctx.Message().Text) + + log := v.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + encodedMsg, err := v.serdes.Deserialize(data) + if err != nil { + log.Warn("can't get ser type", sl.Err(err)) + return ctx.Send("⚠️ Ошибка при расшифровке запроса!") + } + + switch encodedMsg.Type { + case domain.UserType: + view, err := v.userInfo(encodedMsg, uid) + if err != nil { + log.Warn("can't get group info", sl.Err(err)) + return ctx.Send("⚠️ Невозможно получить данного ученика!") + } + return ctx.Send(view, telebot.ModeHTML, telebot.NoPreview) + case domain.GroupType: + view, err := v.groupInfo(encodedMsg, uid) + if err != nil { + log.Warn("can't get group info", sl.Err(err)) + return ctx.Send("⚠️ Невозможно получить данную группу!") + } + return ctx.Send(view, telebot.ModeHTML, telebot.NoPreview) + default: + return ctx.Send("⚠️ Не удалось определить обработчик!") + } +} + +func (v *ViewInformer) userInfo(data *domain.SerializeMessage, uid int64) (string, error) { + const op = "viewInformer.groupInfo" + + if len(data.Data) != 2 { + return "", fmt.Errorf("%s: kid view required 2 fields", op) + } + + full, err := v.viewFetcher.KidView(uid, data.Data[0], data.Data[1]) + if err != nil { + return "", fmt.Errorf("%s: %w", op, err) + } + + return v.GetKidInfoMessage(full), nil } -func ViewInformer(ser Serializator, log *slog.Logger) telebot.HandlerFunc { - return func(ctx telebot.Context) error { - const op = "text.GenerateImage" +func (v *ViewInformer) GetKidInfoMessage(full models.KidView) string { + parentPhone := regexp.MustCompile(`[^0-9+]`).ReplaceAllString(full.Kid.Phone, "") - uid := ctx.Sender().ID - traceID := ctx.Get("trace_id") - data := getData(ctx.Message().Text) + msg := strings.Builder{} + if full.Extra == models.NotAccessible { + msg.WriteString(fmt.Sprintf("⚠️ У вас больше нету доступа к ребенку\n")) + } + msg.WriteString(fmt.Sprintf("%s\n", full.Kid.FullName)) + msg.WriteString(fmt.Sprintf("Возраст: %d\n", full.Kid.Age)) + msg.WriteString(fmt.Sprintf("День рождения: %s\n", full.Kid.BirthDate.Format("2006-01-02"))) + msg.WriteString("\nДанные от аккаунта:\n") + msg.WriteString(fmt.Sprintf("Логин: %s\n", full.Kid.Username)) + msg.WriteString(fmt.Sprintf("Пароль: %s\n", full.Kid.Password)) + msg.WriteString("\nРодитель:\n") + msg.WriteString(fmt.Sprintf("Имя: %s\n", full.Kid.ParentName)) - log = log.With( - slog.String("op", op), - slog.Any("trace_id", traceID), - ) + msg.WriteString(fmt.Sprintf("Телефон: %s 🟩 Whatsapp\n", parentPhone, strings.TrimPrefix(parentPhone, "+"))) + msg.WriteString(fmt.Sprintf("Почта: %s\n", full.Kid.Email)) + msg.WriteString("\nГруппы\n") - serType, err := ser.GetType(data) + groups := full.Kid.Groups + for i := len(groups) - 1; i >= 0; i-- { + msg.WriteString(fmt.Sprintf("%d . %s %s\n", len(groups)-i, groups[i].ID, groups[i].Title, groups[i].Content)) + v, ok := statuses[groups[i].Status] + if !ok { + v = fmt.Sprintf("Статус [%d]", groups[i].Status) + } + msg.WriteString(fmt.Sprintf("%s (%s - %s)\n\n", v, groups[i].StartTime.Format("2006-01-02"), groups[i].EndTime.Format("2006-01-02"))) + } + m := msg.String() + return m +} + +func (v *ViewInformer) groupInfo(data *domain.SerializeMessage, uid int64) (string, error) { + const op = "viewInformer.groupInfo" + + full, err := v.viewFetcher.GroupView(uid, data.Data[0]) + if err != nil { + return "", fmt.Errorf("%s: %w", op, err) + } + + return v.GetGroupInfoMessage(full), nil +} + +func (v *ViewInformer) GetGroupInfoMessage(full models.GroupView) string { + msg := strings.Builder{} + msg.WriteString(fmt.Sprintf("%s %s\n", full.GroupID, full.GroupTitle, full.GroupContent)) + msg.WriteString(fmt.Sprintf("\nСледующая лекция: %s\n", full.NextLessonTime)) + msg.WriteString(fmt.Sprintf("Всего пройдено %d лекций из %d\n", full.LessonsPassed, full.LessonsTotal)) + msg.WriteString(fmt.Sprintf("\nАктивные дети: %d | Выбыло: %d | Всего: %d\n", len(full.ActiveKids), len(full.NotActiveKids), len(full.ActiveKids)+len(full.NotActiveKids))) + msg.WriteString("Активные дети:\n") + + for i, kid := range full.ActiveKids { + ser, err := v.serdes.Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{strconv.Itoa(kid.ID), strconv.Itoa(full.GroupID)}, + }) + if err != nil { + msg.WriteString(fmt.Sprintf("%d. %s\n", i+1, kid.FullName)) + continue + } + + msg.WriteString(fmt.Sprintf("%d. %s\n", i+1, v.botName, ser, kid.FullName)) + } + + msg.WriteString("Выбыли дети:\n") + for i, kid := range full.NotActiveKids { + ser, err := v.serdes.Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{strconv.Itoa(kid.ID), strconv.Itoa(full.GroupID)}, + }) if err != nil { - log.Warn("can't get ser type", sl.Err(err)) - return ctx.Send("⚠️ Ошибка при расшифровке запроса!") + if kid.LastGroup.ID == full.GroupID { + msg.WriteString(fmt.Sprintf("%d. %s (🔴 Выбыл: %s)\n", i+1, kid.FullName, kid.LastGroup.EndTime.Format("2006-01-02"))) + } else { + msg.WriteString(fmt.Sprintf("%d. %s (🟡 Переведен: %s)\n", i+1, kid.FullName, kid.LastGroup.StartTime.Format("2006-01-02"))) + } } - switch serType { - case serdes.UserType: - return nil - case serdes.GroupType: - return nil - default: - return ctx.Send("⚠️ Не удалось определить обработчик") + if kid.LastGroup.ID == full.GroupID { + msg.WriteString(fmt.Sprintf("%d. %s (🔴 Выбыл: %s)\n", i+1, v.botName, ser, kid.FullName, kid.LastGroup.EndTime.Format("2006-01-02"))) + } else { + msg.WriteString(fmt.Sprintf("%d. %s (🟡 Переведен: %s)\n", i+1, v.botName, ser, kid.FullName, kid.LastGroup.StartTime.Format("2006-01-02"))) } } + + return msg.String() } func getData(text string) string { diff --git a/test/mocks/telegram/handlers/groupRefresher.go b/test/mocks/telegram/handlers/groupRefresher.go deleted file mode 100644 index 066e7d9..0000000 --- a/test/mocks/telegram/handlers/groupRefresher.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: algobot/internal/telegram/handlers/callback (interfaces: GroupRefresher) -// -// Generated by this command: -// -// mockgen -destination=./groupRefresher.go -package=mocks algobot/internal/telegram/handlers/callback GroupRefresher -// - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockGroupRefresher is a mock of GroupRefresher interface. -type MockGroupRefresher struct { - ctrl *gomock.Controller - recorder *MockGroupRefresherMockRecorder - isgomock struct{} -} - -// MockGroupRefresherMockRecorder is the mock recorder for MockGroupRefresher. -type MockGroupRefresherMockRecorder struct { - mock *MockGroupRefresher -} - -// NewMockGroupRefresher creates a new mock instance. -func NewMockGroupRefresher(ctrl *gomock.Controller) *MockGroupRefresher { - mock := &MockGroupRefresher{ctrl: ctrl} - mock.recorder = &MockGroupRefresherMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGroupRefresher) EXPECT() *MockGroupRefresherMockRecorder { - return m.recorder -} - -// RefreshGroup mocks base method. -func (m *MockGroupRefresher) RefreshGroup(uid int64, traceID any) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RefreshGroup", uid, traceID) - ret0, _ := ret[0].(error) - return ret0 -} - -// RefreshGroup indicates an expected call of RefreshGroup. -func (mr *MockGroupRefresherMockRecorder) RefreshGroup(uid, traceID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshGroup", reflect.TypeOf((*MockGroupRefresher)(nil).RefreshGroup), uid, traceID) -} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 7e07728..3076b5c 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -19,4 +19,7 @@ package mocks //go:generate mockgen -destination=./chatter_mock.go -package=mocks algobot/internal/telegram/handlers/text Chatter -//go:generate mockgen -destination=./groupRefresher.go -package=mocks algobot/internal/telegram/handlers/callback GroupRefresher +//go:generate mockgen -destination=./groupRefresher_mock.go -package=mocks algobot/internal/telegram/handlers/callback GroupRefresher + +//go:generate mockgen -destination=./viewFetcher_mock.go -package=mocks algobot/internal/telegram/handlers/text ViewFetcher +//go:generate mockgen -destination=./serializator_mock.go -package=mocks algobot/internal/telegram/handlers/text Serializator diff --git a/test/telegram/handlers/viewInformer_test.go b/test/telegram/handlers/viewInformer_test.go new file mode 100644 index 0000000..a17eec7 --- /dev/null +++ b/test/telegram/handlers/viewInformer_test.go @@ -0,0 +1,281 @@ +package test + +import ( + "algobot/internal/domain" + "algobot/internal/domain/models" + "algobot/internal/telegram/handlers/text" + "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" + "time" +) + +func TestViewInformer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + viewFetcher := mocks2.NewMockViewFetcher(ctrl) + serializator := mocks2.NewMockSerializator(ctrl) + botName := "botName" + + mctx := mocks3.NewMockContext(ctrl) + handler := text.NewViewInformer(serializator, viewFetcher, log, botName) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).AnyTimes() + mctx.EXPECT().Message().Return(&tele.Message{Text: "/start abc"}).AnyTimes() + + t.Run("Deserialize err", func(t *testing.T) { + errExp := errors.New("exp") + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(nil, errExp), + mctx.EXPECT().Send("⚠️ Ошибка при расшифровке запроса!").Return(nil).Times(1), + ) + + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("Cant get action handler", func(t *testing.T) { + errExp := errors.New("exp") + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: 222, + Data: []string{}, + }, errExp), + mctx.EXPECT().Send("⚠️ Ошибка при расшифровке запроса!").Return(nil).Times(1), + ) + + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("Kids", func(t *testing.T) { + t.Run("Happy path", func(t *testing.T) { + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"789", "321"}, + }, nil), + viewFetcher.EXPECT().KidView(int64(1), "789", "321").Return(models.KidView{ + Extra: "", + Kid: models.Kid{ + FullName: "Алексей Смирнов", + ParentName: "Мария Смирнова", + Email: "alexey.smirnov@example.com", + Phone: "+7 (912) 345-67-89", + Age: 10, + BirthDate: time.Date(2014, 3, 15, 0, 0, 0, 0, time.UTC), + Username: "aleksey10", + Password: "securepassword123", + Groups: []models.KidViewGroup{ + { + ID: 101, + Title: "Основы программирования", + Content: "Изучение Scratch и базовых алгоритмов", + Status: 0, + StartTime: time.Date(2024, 9, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2024, 12, 15, 12, 0, 0, 0, time.UTC), + }, + { + ID: 102, + Title: "Веб-разработка для детей", + Content: "HTML, CSS и основы JavaScript", + Status: 10, + StartTime: time.Date(2025, 1, 10, 14, 0, 0, 0, time.UTC), + EndTime: time.Date(2025, 3, 30, 16, 0, 0, 0, time.UTC), + }, + { + ID: 103, + Title: "Робототехника", + Content: "Сборка и программирование LEGO-роботов", + Status: 20, + StartTime: time.Date(2025, 4, 5, 9, 0, 0, 0, time.UTC), + EndTime: time.Date(2025, 6, 20, 11, 0, 0, 0, time.UTC), + }, + }, + }, + }, nil), + mctx.EXPECT().Send("Алексей Смирнов\nВозраст: 10\nДень рождения: 2014-03-15\n\nДанные от аккаунта:\nЛогин: aleksey10\nПароль: securepassword123\n\nРодитель:\nИмя: Мария Смирнова\nТелефон: +79123456789 🟩 Whatsapp\nПочта: alexey.smirnov@example.com\n\nГруппы\n1 . Робототехника Сборка и программирование LEGO-роботов\n🔴 Выбыл (2025-04-05 - 2025-06-20)\n\n2 . Веб-разработка для детей HTML, CSS и основы JavaScript\n🟡 Переведен (2025-01-10 - 2025-03-30)\n\n3 . Основы программирования Изучение Scratch и базовых алгоритмов\n🟢 Учится (2024-09-01 - 2024-12-15)\n\n", tele.ModeHTML, tele.NoPreview).Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("HappyPath kid not accessebly", func(t *testing.T) { + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"789", "321"}, + }, nil), + viewFetcher.EXPECT().KidView(int64(1), "789", "321").Return(models.KidView{ + Extra: models.NotAccessible, + Kid: models.Kid{ + FullName: "Алексей Смирнов", + ParentName: "Мария Смирнова", + Email: "alexey.smirnov@example.com", + Phone: "+7 (912) 345-67-89", + Age: 10, + BirthDate: time.Date(2014, 3, 15, 0, 0, 0, 0, time.UTC), + Username: "aleksey10", + Password: "securepassword123", + Groups: []models.KidViewGroup{ + { + ID: 101, + Title: "Основы программирования", + Content: "Изучение Scratch и базовых алгоритмов", + Status: 0, + StartTime: time.Date(2024, 9, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2024, 12, 15, 12, 0, 0, 0, time.UTC), + }, + { + ID: 102, + Title: "Веб-разработка для детей", + Content: "HTML, CSS и основы JavaScript", + Status: 10, + StartTime: time.Date(2025, 1, 10, 14, 0, 0, 0, time.UTC), + EndTime: time.Date(2025, 3, 30, 16, 0, 0, 0, time.UTC), + }, + { + ID: 103, + Title: "Робототехника", + Content: "Сборка и программирование LEGO-роботов", + Status: 20, + StartTime: time.Date(2025, 4, 5, 9, 0, 0, 0, time.UTC), + EndTime: time.Date(2025, 6, 20, 11, 0, 0, 0, time.UTC), + }, + }, + }, + }, nil), + mctx.EXPECT().Send("⚠️ У вас больше нету доступа к ребенку\nАлексей Смирнов\nВозраст: 10\nДень рождения: 2014-03-15\n\nДанные от аккаунта:\nЛогин: aleksey10\nПароль: securepassword123\n\nРодитель:\nИмя: Мария Смирнова\nТелефон: +79123456789 🟩 Whatsapp\nПочта: alexey.smirnov@example.com\n\nГруппы\n1 . Робототехника Сборка и программирование LEGO-роботов\n🔴 Выбыл (2025-04-05 - 2025-06-20)\n\n2 . Веб-разработка для детей HTML, CSS и основы JavaScript\n🟡 Переведен (2025-01-10 - 2025-03-30)\n\n3 . Основы программирования Изучение Scratch и базовых алгоритмов\n🟢 Учится (2024-09-01 - 2024-12-15)\n\n", tele.ModeHTML, tele.NoPreview).Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("userInfo error", func(t *testing.T) { + t.Run("data len not 2", func(t *testing.T) { + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"789"}, + }, nil), + mctx.EXPECT().Send("⚠️ Невозможно получить данного ученика!").Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("KidView return err", func(t *testing.T) { + errExp := errors.New("errExp") + + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"789", "123"}, + }, nil), + viewFetcher.EXPECT().KidView(int64(1), "789", "123").Return(models.KidView{}, errExp).Times(1), + mctx.EXPECT().Send("⚠️ Невозможно получить данного ученика!").Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + }) + }) + t.Run("Groups", func(t *testing.T) { + t.Run("Happy path", func(t *testing.T) { + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{"123"}, + }, nil), + viewFetcher.EXPECT().GroupView(int64(1), "123").Return(models.GroupView{ + GroupID: 1, + GroupTitle: "Математика для детей", + GroupContent: "Основы арифметики и геометрии", + NextLessonTime: "2023-10-01T10:00:00Z", + LessonsTotal: 12, + LessonsPassed: 5, + ActiveKids: []models.GroupKid{ + { + ID: 101, + FullName: "Иван Иванов", + LastGroup: models.KidGroup{ + ID: 1, + StartTime: time.Date(2023, 9, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 9, 1, 12, 0, 0, 0, time.UTC), + }, + }, + { + ID: 102, + FullName: "Мария Петровна", + LastGroup: models.KidGroup{ + ID: 2, + StartTime: time.Date(2023, 9, 5, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 9, 5, 12, 0, 0, 0, time.UTC), + }, + }, + { + ID: 103, + FullName: "Алексей Сидоров", + LastGroup: models.KidGroup{ + ID: 3, + StartTime: time.Date(2023, 9, 10, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 9, 10, 12, 0, 0, 0, time.UTC), + }, + }, + }, + NotActiveKids: []models.GroupKid{ + { + ID: 104, + FullName: "Ольга Васильева", + LastGroup: models.KidGroup{ + ID: 4, + StartTime: time.Date(2023, 8, 25, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 8, 25, 12, 0, 0, 0, time.UTC), + }, + }, + }, + }, nil), + serializator.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"101", "1"}, + }).Return("1", nil).Times(1), + serializator.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"102", "1"}, + }).Return("2", nil).Times(1), + serializator.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"103", "1"}, + }).Return("3", nil).Times(1), + serializator.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.UserType, + Data: []string{"104", "1"}, + }).Return("4", nil).Times(1), + + mctx.EXPECT().Send("Математика для детей Основы арифметики и геометрии\n\nСледующая лекция: 2023-10-01T10:00:00Z\nВсего пройдено 5 лекций из 12\n\nАктивные дети: 3 | Выбыло: 1 | Всего: 4\nАктивные дети:\n1. Иван Иванов\n2. Мария Петровна\n3. Алексей Сидоров\nВыбыли дети:\n1. Ольга Васильева (🟡 Переведен: 2023-08-25)\n", tele.ModeHTML, tele.NoPreview).Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + t.Run("groupInfo error", func(t *testing.T) { + t.Run("GroupView return err", func(t *testing.T) { + errExp := errors.New("errExp") + + gomock.InOrder( + serializator.EXPECT().Deserialize("abc").Return(&domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{"123"}, + }, nil), + viewFetcher.EXPECT().GroupView(int64(1), "123").Return(models.GroupView{}, errExp).Times(1), + mctx.EXPECT().Send("⚠️ Невозможно получить данную группу!").Return(nil).Times(1), + ) + err := handler.ServeContext(mctx) + assert.NoError(t, err) + }) + }) + }) + +} From b1452f28e8aa05d0c70cbf6dcd578fa566a0935d Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 14:13:34 +0300 Subject: [PATCH 28/44] add service backoffice + tests --- internal/domain/backoffice/fullGroupInfo.go | 165 ++++++ internal/domain/backoffice/kidVIew.go | 23 + internal/domain/backoffice/namesByGroup.go | 80 +++ internal/domain/backoffice/student.go | 25 + internal/lib/backoffice/backoffice.go | 3 +- internal/lib/backoffice/groupView.go | 31 ++ internal/lib/mappers/bo-to-svc.go | 65 +++ internal/services/backoffice/backoffice.go | 18 + internal/services/backoffice/groupView.go | 63 +++ internal/services/backoffice/kidView.go | 65 +++ .../telegram/handlers/text/viewInformer.go | 16 +- test/mocks/services/mockgen.go | 4 + test/services/backoffice_test.go | 469 ++++++++++++++++++ test/telegram/handlers/viewInformer_test.go | 11 +- 14 files changed, 1023 insertions(+), 15 deletions(-) create mode 100644 internal/domain/backoffice/fullGroupInfo.go create mode 100644 internal/domain/backoffice/kidVIew.go create mode 100644 internal/domain/backoffice/namesByGroup.go create mode 100644 internal/domain/backoffice/student.go create mode 100644 internal/lib/backoffice/groupView.go create mode 100644 internal/lib/mappers/bo-to-svc.go create mode 100644 internal/services/backoffice/backoffice.go create mode 100644 internal/services/backoffice/groupView.go create mode 100644 internal/services/backoffice/kidView.go create mode 100644 test/services/backoffice_test.go diff --git a/internal/domain/backoffice/fullGroupInfo.go b/internal/domain/backoffice/fullGroupInfo.go new file mode 100644 index 0000000..555d362 --- /dev/null +++ b/internal/domain/backoffice/fullGroupInfo.go @@ -0,0 +1,165 @@ +package backoffice + +type GroupInfo struct { + Status string `json:"status"` + Data GroupDataFull `json:"data"` +} +type GroupDataFull struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Type TypeFull `json:"type"` + Status StatusFull `json:"status"` + StatusChangedAt string `json:"status_changed_at"` + StartTime string `json:"start_time"` + NextLessonTime string `json:"next_lesson_time"` + LessonsTotal int `json:"lessons_total"` + LessonsPassed int `json:"lessons_passed"` + HardwareNeeded int `json:"hardware_needed"` + Branch BranchFull `json:"branch"` + Venue VenueFull `json:"venue"` + Curator UserFull `json:"curator"` + Teacher TeacherFull `json:"teacher"` + Teachers []TeacherFull `json:"teachers"` + ClientManager interface{} `json:"client_manager"` + Course CourseFull `json:"course"` + LanguageID interface{} `json:"language_id"` + Journal bool `json:"journal"` + ShowJournal bool `json:"show_journal"` + ShowOnlineRoom bool `json:"showOnlineRoom"` + IsOnline bool `json:"isOnline"` + ActiveStudentCount int `json:"active_student_count"` + OnlineRoomURL string `json:"online_room_url"` + UseClientManager int `json:"use_client_manager"` + DisplayLessonDurationInMinutes int `json:"display_lesson_duration_in_minutes"` + DeletedAt interface{} `json:"deleted_at"` + DeletedBy interface{} `json:"deleted_by"` + PriorityLevel PriorityLevelFull `json:"priority_level"` + IsFull bool `json:"is_full"` + CreatedAt string `json:"created_at"` + CreatedBy UserFull `json:"created_by"` + Related RelatedFull `json:"_related"` +} + +type StatusFull struct { + Value int `json:"value"` + Label string `json:"label"` + Tag string `json:"tag"` +} + +type TypeFull struct { + Value string `json:"value"` + Label string `json:"label"` + Tag string `json:"tag"` +} + +type ProfileFull struct { + PhotoURL string `json:"photo_url"` + Promo string `json:"promo"` +} + +type LinksFull struct { + Self string `json:"self"` +} + +type BranchFull struct { + ID int `json:"id"` + Title string `json:"title"` + Code string `json:"code"` + Description string `json:"description"` + Phone string `json:"phone"` + Email string `json:"email"` + SiteURL string `json:"site_url"` + TemplateVersion int `json:"templateVersion"` + UseAmo bool `json:"use_amo"` + AmoConfigID int `json:"amoConfigId"` + ShowFinanceInfo bool `json:"show_finance_info"` + LmsDisplayStudentCredentials bool `json:"lms_display_student_credentials"` + ShowOnlineRoomURLField int `json:"show_online_room_url_field"` + UseSms bool `json:"use_sms"` + LanguageID int `json:"language_id"` + OrderName int `json:"order_name"` + UseFullyPaidLabel int `json:"use_fully_paid_label"` + BrandName string `json:"brandName"` + MaxCountStudentsForShowOnline int `json:"max_count_students_for_show_online"` + IsFillPaymentSystem bool `json:"isFillPaymentSystem"` + FirstLessonNoRoyalty int `json:"firstLessonNoRoyalty"` + RootBranchID int `json:"root_branch_id"` +} + +type VenueFull struct { + ID int `json:"id"` + Title string `json:"title"` + Address string `json:"address"` + ContactName string `json:"contact_name"` + ContactEmail string `json:"contact_email"` + ContactPhone string `json:"contact_phone"` + Links LinksFull `json:"_links"` +} + +type UserFull struct { + ID int `json:"id"` + Username string `json:"username"` + Phone string `json:"phone"` + Email string `json:"email"` + Name string `json:"name"` + Profile ProfileFull `json:"profile"` + Status int `json:"status"` + Links LinksFull `json:"_links"` +} + +type TeacherFull struct { + ID int `json:"id"` + Username string `json:"username"` + Phone string `json:"phone"` + Email string `json:"email"` + Name string `json:"name"` + Profile ProfileFull `json:"profile"` + AllowedUserCourses []AllowedUserCourseFull `json:"allowedUserCourses"` + Status int `json:"status"` + Links LinksFull `json:"_links"` +} + +type AllowedUserCourseFull struct { + UserID int `json:"userId"` + CourseID int `json:"courseId"` + IsAllowed int `json:"isAllowed"` +} + +type CourseTypeFull struct { + ID int `json:"id"` + Title string `json:"title"` + Code string `json:"code"` +} + +type CourseFull struct { + ID int `json:"id"` + Name string `json:"name"` + GUID string `json:"guid"` + Description string `json:"description"` + ContentType string `json:"contentType"` + CourseType CourseTypeFull `json:"courseType"` + LessonsCount int `json:"lessons_count"` + GroupLessonsAmount int `json:"group_lessons_amount"` + LessonsCountFormatted string `json:"lessons_count_formatted"` + GroupLessonsAmountFormatted string `json:"group_lessons_amount_formatted"` + IsDeleted int `json:"is_deleted"` + Links LinksFull `json:"_links"` +} + +type PriorityLevelFull struct { + Value string `json:"value"` + Label string `json:"label"` + Tag string `json:"tag"` +} + +type RelatedFull struct { + Statuses []StatusFull `json:"statuses"` + Types []TypeFull `json:"types"` + PriorityLevels []PriorityLevelFull `json:"priority_levels"` +} + +type FullGroupInfo struct { + Status string `json:"status"` + Data GroupDataFull `json:"data"` +} diff --git a/internal/domain/backoffice/kidVIew.go b/internal/domain/backoffice/kidVIew.go new file mode 100644 index 0000000..ed9a268 --- /dev/null +++ b/internal/domain/backoffice/kidVIew.go @@ -0,0 +1,23 @@ +package backoffice + +import "time" + +type KidView struct { + Status string `json:"status"` + Data Student `json:"data"` +} + +type GroupKidInfo struct { + ID int `json:"id"` + GroupStudentID int `json:"groupStudentId"` + Title string `json:"title"` + Content string `json:"content"` + Track int `json:"track"` + Status int `json:"status"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + CourseID int `json:"courseId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt any `json:"deletedAt"` +} diff --git a/internal/domain/backoffice/namesByGroup.go b/internal/domain/backoffice/namesByGroup.go new file mode 100644 index 0000000..8d95826 --- /dev/null +++ b/internal/domain/backoffice/namesByGroup.go @@ -0,0 +1,80 @@ +package backoffice + +import "time" + +type NamesByGroup struct { + Status string `json:"status"` + Data GroupData `json:"data"` +} +type GroupData struct { + Items []Student `json:"items"` +} + +type Group struct { + ID int `json:"id"` + GroupStudentID int `json:"groupStudentId"` + Title string `json:"title"` + Content string `json:"content"` + Track int `json:"track"` + Status int `json:"status"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + CourseID int `json:"courseId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt interface{} `json:"deletedAt"` +} + +type Links struct { + Self SelfLink `json:"self"` +} + +type SelfLink struct { + Href string `json:"href"` +} + +type KidsStats struct { + Status string `json:"status"` + Data []KidStat `json:"data"` +} + +type KidStat struct { + StudentID int `json:"student_id"` + Attendance []Attendance `json:"attendance"` +} + +type Attendance struct { + LessonID int `json:"lesson_id"` + LessonTitle string `json:"lesson_title"` + StartTimeFormatted string `json:"start_time_formatted"` + Status string `json:"status"` +} + +type KidsMessages struct { + Status string `json:"status"` + Data MessagesData `json:"data"` +} + +type MessagesData struct { + Projects []Message `json:"projects"` +} + +type Message struct { + UID string `json:"uid"` + New bool `json:"new"` + SenderID int `json:"senderId"` + SenderScope string `json:"senderScope"` + Type string `json:"type"` + Content string `json:"content"` + Name string `json:"name"` + LastTime string `json:"lastTime"` + Title string `json:"title"` + Link string `json:"link"` +} + +type AllGroupsUser struct { + Title string + GroupId string + TimeLesson string + RegularTime string +} diff --git a/internal/domain/backoffice/student.go b/internal/domain/backoffice/student.go new file mode 100644 index 0000000..a8e76db --- /dev/null +++ b/internal/domain/backoffice/student.go @@ -0,0 +1,25 @@ +package backoffice + +import "time" + +type Student struct { + ID int `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + FullName string `json:"fullName"` + ParentName string `json:"parentName"` + Email string `json:"email"` + HasLaptop int `json:"hasLaptop"` + Phone string `json:"phone"` + Age int `json:"age"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt interface{} `json:"deletedAt"` + HasBranchAccess bool `json:"hasBranchAccess"` + Username string `json:"username"` + Password string `json:"password"` + LastGroup Group `json:"lastGroup"` + Groups []Group `json:"groups"` + Links Links `json:"_links"` +} diff --git a/internal/lib/backoffice/backoffice.go b/internal/lib/backoffice/backoffice.go index 64c8938..c6fb507 100644 --- a/internal/lib/backoffice/backoffice.go +++ b/internal/lib/backoffice/backoffice.go @@ -14,7 +14,8 @@ import ( ) var ( - ErrBadCode = errors.New("bad code") + ErrBadCode = errors.New("bad code") + ErrNotFound = errors.New("not found") ) type Option func(*Backoffice) diff --git a/internal/lib/backoffice/groupView.go b/internal/lib/backoffice/groupView.go new file mode 100644 index 0000000..2788c60 --- /dev/null +++ b/internal/lib/backoffice/groupView.go @@ -0,0 +1,31 @@ +package backoffice + +import ( + "algobot/internal/domain/models" + "encoding/json" + "fmt" +) + +func (bo *Backoffice) GroupView(uid int64, groupID string, cookie string) (models.GroupView, error) { + const op = "backoffice.GetGroupView" + + req, err := bo.createReq("GET", "/api/v1/group/"+groupID, cookie, map[string]string{ + "expand": "venue,teacher,curator,branch", + }, nil) + if err != nil { + return models.GroupView{}, fmt.Errorf("%s err create req: %w", op, err) + } + + data, err := bo.doReq(req) + if err != nil { + return models.GroupView{}, fmt.Errorf("%s err doReq: %w", op, err) + } + + var response models.GroupView + err = json.NewDecoder(data.Body).Decode(&response) + if err != nil { + return models.GroupView{}, fmt.Errorf("%s err decode json: %w", op, err) + } + + return models.GroupView{}, nil +} diff --git a/internal/lib/mappers/bo-to-svc.go b/internal/lib/mappers/bo-to-svc.go new file mode 100644 index 0000000..a61266b --- /dev/null +++ b/internal/lib/mappers/bo-to-svc.go @@ -0,0 +1,65 @@ +package mappers + +import ( + "algobot/internal/domain/backoffice" + "algobot/internal/domain/models" +) + +func MapKid(item backoffice.Student) models.GroupKid { + m := models.GroupKid{} + m.ID = item.ID + m.FullName = item.FullName + m.LastGroup = MapGroup(item.LastGroup) + + return m +} + +func MapGroup(group backoffice.Group) models.KidGroup { + m := models.KidGroup{} + m.ID = group.ID + m.StartTime = group.StartTime + m.EndTime = group.EndTime + + return m +} + +func MapKidView(view backoffice.KidView) models.KidView { + m := models.KidView{} + m.Kid.FullName = view.Data.FullName + m.Kid.ParentName = view.Data.ParentName + m.Kid.Email = view.Data.Email + m.Kid.Phone = view.Data.Phone + m.Kid.Age = view.Data.Age + m.Kid.BirthDate = view.Data.BirthDate + m.Kid.Username = view.Data.Username + m.Kid.Password = view.Data.Password + + m.Kid.Groups = make([]models.KidViewGroup, len(view.Data.Groups)) + for i, group := range view.Data.Groups { + m.Kid.Groups[i] = models.KidViewGroup{ + ID: group.ID, + Title: group.Title, + Content: group.Content, + Status: group.Status, + StartTime: group.StartTime, + EndTime: group.EndTime, + } + } + + return m +} + +func MapGroups(groups []backoffice.Group) []models.KidViewGroup { + mappedGroups := make([]models.KidViewGroup, len(groups)) + for i, group := range groups { + mappedGroups[i] = models.KidViewGroup{ + ID: group.ID, + Title: group.Title, + Content: group.Content, + Status: group.Status, + StartTime: group.StartTime, + EndTime: group.EndTime, + } + } + return mappedGroups +} diff --git a/internal/services/backoffice/backoffice.go b/internal/services/backoffice/backoffice.go new file mode 100644 index 0000000..f8df3c2 --- /dev/null +++ b/internal/services/backoffice/backoffice.go @@ -0,0 +1,18 @@ +package backoffice + +import "log/slog" + +type CookieGetter interface { + Cookies(uid int64) (string, error) +} + +type Backoffice struct { + log *slog.Logger + cookieGetter CookieGetter + groupView GroupView + kidViewer KidViewer +} + +func NewBackoffice(log *slog.Logger, cookieGetter CookieGetter, groupView GroupView, kidViewer KidViewer) *Backoffice { + return &Backoffice{log: log, cookieGetter: cookieGetter, groupView: groupView, kidViewer: kidViewer} +} diff --git a/internal/services/backoffice/groupView.go b/internal/services/backoffice/groupView.go new file mode 100644 index 0000000..2353edb --- /dev/null +++ b/internal/services/backoffice/groupView.go @@ -0,0 +1,63 @@ +package backoffice + +import ( + "algobot/internal/domain/backoffice" + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "algobot/internal/lib/mappers" + "fmt" + "log/slog" + "strconv" +) + +type GroupView interface { + GroupView(groupID string, cookie string) (backoffice.GroupInfo, error) + KidsNamesByGroup(groupId string, cookie string) (backoffice.NamesByGroup, error) +} + +func (bo *Backoffice) GroupView(uid int64, groupID string, traceID interface{}) (models.GroupView, error) { + const op = "services.backoffice.GetGroupView" + log := bo.log.With( + slog.String("op", op), + slog.Any("traceID", traceID), + ) + + cookie, err := bo.cookieGetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return models.GroupView{}, fmt.Errorf("%s failed to get cookies: %w", op, err) + } + + grView, err := bo.groupView.GroupView(groupID, cookie) + if err != nil { + log.Warn("failed to group view", sl.Err(err)) + return models.GroupView{}, fmt.Errorf("%s failed to group view: %w", op, err) + } + kidsNames, err := bo.groupView.KidsNamesByGroup(groupID, cookie) + if err != nil { + log.Warn("failed to get kids names", sl.Err(err)) + return models.GroupView{}, fmt.Errorf("%s failed to get kids names: %w", op, err) + } + + return mapResp(grView, kidsNames, groupID), nil +} + +func mapResp(info backoffice.GroupInfo, names backoffice.NamesByGroup, groupID string) models.GroupView { + m := models.GroupView{} + m.GroupID = info.Data.ID + m.GroupTitle = info.Data.Title + m.GroupContent = info.Data.Content + m.NextLessonTime = info.Data.NextLessonTime + m.LessonsPassed = info.Data.LessonsPassed + m.LessonsTotal = info.Data.LessonsTotal + + for _, item := range names.Data.Items { + if strconv.Itoa(item.LastGroup.ID) == groupID && item.LastGroup.Status == 0 { + m.ActiveKids = append(m.ActiveKids, mappers.MapKid(item)) + } else { + m.NotActiveKids = append(m.NotActiveKids, mappers.MapKid(item)) + } + } + + return m +} diff --git a/internal/services/backoffice/kidView.go b/internal/services/backoffice/kidView.go new file mode 100644 index 0000000..95443b6 --- /dev/null +++ b/internal/services/backoffice/kidView.go @@ -0,0 +1,65 @@ +package backoffice + +import ( + backoffice2 "algobot/internal/domain/backoffice" + "algobot/internal/domain/models" + "algobot/internal/lib/backoffice" + "algobot/internal/lib/logger/sl" + "algobot/internal/lib/mappers" + "errors" + "fmt" + "log/slog" + "strconv" +) + +type KidViewer interface { + KidView(kidID string, cookie string) (backoffice2.KidView, error) + KidsNamesByGroup(groupId string, cookie string) (backoffice2.NamesByGroup, error) +} + +func (bo *Backoffice) KidView(uid int64, kidID string, groupId string, traceID interface{}) (models.KidView, error) { + const op = "services.backoffice.KidView" + log := bo.log.With( + slog.String("op", op), + slog.Any("traceID", traceID), + ) + + cookie, err := bo.cookieGetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return models.KidView{}, fmt.Errorf("%s failed to get cookies: %w", op, err) + } + + view, err := bo.kidViewer.KidView(kidID, cookie) + if err != nil { + if errors.Is(err, backoffice.ErrNotFound) { // TODO : maybe refactor into single request + info, err := bo.kidViewer.KidsNamesByGroup(groupId, cookie) + if err != nil { + log.Warn("failed to get kids names by group", sl.Err(err)) + return models.KidView{}, fmt.Errorf("%s failed to get kids names by group: %w", op, err) + } + for _, item := range info.Data.Items { + if strconv.Itoa(item.ID) == kidID { + return models.KidView{ + Extra: models.NotAccessible, + Kid: models.Kid{ + FullName: item.FullName, + ParentName: item.ParentName, + Email: item.Email, + Phone: item.Phone, + Age: item.Age, + BirthDate: item.BirthDate, + Username: item.Username, + Password: item.Password, + Groups: mappers.MapGroups(item.Groups), + }, + }, nil + } + } + } + log.Warn("failed to kid view", sl.Err(err)) + return models.KidView{}, fmt.Errorf("%s failed to kid view: %w", op, err) + } + + return mappers.MapKidView(view), nil +} diff --git a/internal/telegram/handlers/text/viewInformer.go b/internal/telegram/handlers/text/viewInformer.go index df70c24..ea65f1f 100644 --- a/internal/telegram/handlers/text/viewInformer.go +++ b/internal/telegram/handlers/text/viewInformer.go @@ -19,8 +19,8 @@ var statuses = map[int]string{ } type ViewFetcher interface { - GroupView(uid int64, groupID string) (models.GroupView, error) - KidView(uid int64, kidID string, groupId string) (models.KidView, error) + GroupView(uid int64, groupID string, traceID interface{}) (models.GroupView, error) + KidView(uid int64, kidID string, groupId string, traceID interface{}) (models.KidView, error) } type Serializator interface { @@ -59,14 +59,14 @@ func (v *ViewInformer) ServeContext(ctx telebot.Context) error { switch encodedMsg.Type { case domain.UserType: - view, err := v.userInfo(encodedMsg, uid) + view, err := v.userInfo(encodedMsg, uid, traceID) if err != nil { log.Warn("can't get group info", sl.Err(err)) return ctx.Send("⚠️ Невозможно получить данного ученика!") } return ctx.Send(view, telebot.ModeHTML, telebot.NoPreview) case domain.GroupType: - view, err := v.groupInfo(encodedMsg, uid) + view, err := v.groupInfo(encodedMsg, uid, traceID) if err != nil { log.Warn("can't get group info", sl.Err(err)) return ctx.Send("⚠️ Невозможно получить данную группу!") @@ -77,14 +77,14 @@ func (v *ViewInformer) ServeContext(ctx telebot.Context) error { } } -func (v *ViewInformer) userInfo(data *domain.SerializeMessage, uid int64) (string, error) { +func (v *ViewInformer) userInfo(data *domain.SerializeMessage, uid int64, traceID interface{}) (string, error) { const op = "viewInformer.groupInfo" if len(data.Data) != 2 { return "", fmt.Errorf("%s: kid view required 2 fields", op) } - full, err := v.viewFetcher.KidView(uid, data.Data[0], data.Data[1]) + full, err := v.viewFetcher.KidView(uid, data.Data[0], data.Data[1], traceID) if err != nil { return "", fmt.Errorf("%s: %w", op, err) } @@ -125,10 +125,10 @@ func (v *ViewInformer) GetKidInfoMessage(full models.KidView) string { return m } -func (v *ViewInformer) groupInfo(data *domain.SerializeMessage, uid int64) (string, error) { +func (v *ViewInformer) groupInfo(data *domain.SerializeMessage, uid int64, traceID interface{}) (string, error) { const op = "viewInformer.groupInfo" - full, err := v.viewFetcher.GroupView(uid, data.Data[0]) + full, err := v.viewFetcher.GroupView(uid, data.Data[0], traceID) if err != nil { return "", fmt.Errorf("%s: %w", op, err) } diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index 8b5b212..57a4e2e 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -5,3 +5,7 @@ package mocks //go:generate mockgen -destination=./groupGetter_mock.go -package=mocks algobot/internal/services/groups GroupGetter //go:generate mockgen -destination=./domainSetter_mock.go -package=mocks algobot/internal/services/groups DomainSetter //go:generate mockgen -destination=./groupFetcher_mock.go -package=mocks algobot/internal/services/groups GroupFetcher + +//go:generate mockgen -destination=./groupView_mock.go -package=mocks algobot/internal/services/backoffice GroupView +//go:generate mockgen -destination=./kidViewer_mock.go -package=mocks algobot/internal/services/backoffice KidViewer +//go:generate mockgen -destination=./cookieGetter_mock.go -package=mocks algobot/internal/services/backoffice CookieGetter diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go new file mode 100644 index 0000000..509bbd7 --- /dev/null +++ b/test/services/backoffice_test.go @@ -0,0 +1,469 @@ +package test + +import ( + backoffice2 "algobot/internal/domain/backoffice" + "algobot/internal/domain/models" + backoffice3 "algobot/internal/lib/backoffice" + "algobot/internal/services/backoffice" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/services" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func TestBackoffice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + cookieGetter := mocks2.NewMockCookieGetter(ctrl) + groupView := mocks2.NewMockGroupView(ctrl) + kidViewer := mocks2.NewMockKidViewer(ctrl) + + sbo := backoffice.NewBackoffice(log, cookieGetter, groupView, kidViewer) + t.Run("KidView", func(t *testing.T) { + uid := int64(1) + kidID := "1" + groupID := "2" + traceID := "trace" + cookie := "cookie" + errExp := errors.New("err exp") + + t.Run("happy path by KidView", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + kidViewer.EXPECT().KidView(kidID, cookie).Return(KidViewBackoffice, nil).Times(1), + ) + + view, err := sbo.KidView(uid, kidID, groupID, traceID) + assert.NoError(t, err) + assert.Equal(t, kidExpected, view) + }) + t.Run("happy path by KidsNamesByGroup", func(t *testing.T) { + kidExpectedP := kidExpected + kidExpectedP.Extra = models.NotAccessible + + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + kidViewer.EXPECT().KidView(kidID, cookie).Return(backoffice2.KidView{}, backoffice3.ErrNotFound).Times(1), + kidViewer.EXPECT().KidsNamesByGroup(groupID, cookie).Return(kidsNamesByGroupBackoffice, nil).Times(1), + ) + + view, err := sbo.KidView(uid, kidID, groupID, traceID) + assert.NoError(t, err) + assert.Equal(t, kidExpectedP, view) + }) + t.Run("cookie returns err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return("", errExp).Times(1), + ) + + _, err := sbo.KidView(uid, kidID, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("KidView returns err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + kidViewer.EXPECT().KidView(kidID, cookie).Return(backoffice2.KidView{}, errExp).Times(1), + ) + + _, err := sbo.KidView(uid, kidID, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("KidsNamesByGroup returns err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + kidViewer.EXPECT().KidView(kidID, cookie).Return(backoffice2.KidView{}, backoffice3.ErrNotFound).Times(1), + kidViewer.EXPECT().KidsNamesByGroup(groupID, cookie).Return(backoffice2.NamesByGroup{}, errExp).Times(1), + ) + + _, err := sbo.KidView(uid, kidID, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + }) + t.Run("Group view", func(t *testing.T) { + uid := int64(1) + kidID := "1" + groupID := "2" + traceID := "trace" + cookie := "cookie" + errExp := errors.New("err exp") + + _ = errExp + _ = kidID + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + groupView.EXPECT().GroupView(groupID, cookie).Return(groupInfoBackoffice, nil).Times(1), + groupView.EXPECT().KidsNamesByGroup(groupID, cookie).Return(kidsNamesByGroupBackoffice, nil).Times(1), + ) + + view, err := sbo.GroupView(uid, groupID, traceID) + assert.NoError(t, err) + assert.Equal(t, expectedGroupView, view) + }) + t.Run("cookie return err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return("", errExp).Times(1), + ) + + _, err := sbo.GroupView(uid, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("GroupView return err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + groupView.EXPECT().GroupView(groupID, cookie).Return(groupInfoBackoffice, errExp).Times(1), + ) + + _, err := sbo.GroupView(uid, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("GroupView return err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + groupView.EXPECT().GroupView(groupID, cookie).Return(groupInfoBackoffice, nil).Times(1), + groupView.EXPECT().KidsNamesByGroup(groupID, cookie).Return(kidsNamesByGroupBackoffice, errExp).Times(1), + ) + + _, err := sbo.GroupView(uid, groupID, traceID) + assert.ErrorIs(t, err, errExp) + }) + }) +} + +var KidViewBackoffice = backoffice2.KidView{ + Status: "Активен", + Data: backoffice2.Student{ + ID: 101, + FirstName: "Иван", + LastName: "Иванов", + FullName: "Иван Иванов", + ParentName: "Мария Ивановна Иванова", + Email: "ivanov@example.com", + HasLaptop: 1, + Phone: "+79171234567", + Age: 10, + BirthDate: time.Date(2013, 5, 15, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2022, 9, 1, 12, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 3, 15, 14, 30, 0, 0, time.UTC), + DeletedAt: nil, + HasBranchAccess: true, + Username: "ivan101", + Password: "securepassword123", + LastGroup: backoffice2.Group{ + ID: 201, + GroupStudentID: 301, + Title: "Группа по математике", + Content: "Основы математики для младших школьников", + Track: 1, + Status: 1, + StartTime: time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 6, 30, 11, 0, 0, 0, time.UTC), + CourseID: 401, + CreatedAt: time.Date(2022, 12, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 2, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + Groups: []backoffice2.Group{ + { + ID: 201, + GroupStudentID: 301, + Title: "Группа по математике", + Content: "Основы математики для младших школьников", + Track: 1, + Status: 1, + StartTime: time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 6, 30, 11, 0, 0, 0, time.UTC), + CourseID: 401, + CreatedAt: time.Date(2022, 12, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 2, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + { + ID: 202, + GroupStudentID: 302, + Title: "Уравнения и геометрия", + Content: "Разбор уравнений и элементы геометрии", + Track: 2, + Status: 1, + StartTime: time.Date(2023, 7, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 12, 31, 11, 0, 0, 0, time.UTC), + CourseID: 402, + CreatedAt: time.Date(2023, 6, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 6, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + }, + }, +} +var kidExpected = models.KidView{ + Extra: "", + Kid: models.Kid{ + FullName: "Иван Иванов", + ParentName: "Мария Ивановна Иванова", + Email: "ivanov@example.com", + Phone: "+79171234567", + Age: 10, + BirthDate: time.Date(2013, 5, 15, 0, 0, 0, 0, time.UTC), + Username: "ivan101", + Password: "securepassword123", + Groups: []models.KidViewGroup{ + { + ID: 201, + Title: "Группа по математике", + Content: "Основы математики для младших школьников", + Status: 1, + StartTime: time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 6, 30, 11, 0, 0, 0, time.UTC), + }, + { + ID: 202, + Title: "Уравнения и геометрия", + Content: "Разбор уравнений и элементы геометрии", + Status: 1, + StartTime: time.Date(2023, 7, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 12, 31, 11, 0, 0, 0, time.UTC), + }, + }, + }, +} + +var kidsNamesByGroupBackoffice = backoffice2.NamesByGroup{ + Status: "success", + Data: backoffice2.GroupData{ + Items: []backoffice2.Student{ + { + ID: 1, + FirstName: "Иван", + LastName: "Иванов", + FullName: "Иван Иванов", + ParentName: "Мария Ивановна Иванова", + Email: "ivanov@example.com", + HasLaptop: 1, + Phone: "+79171234567", + Age: 10, + BirthDate: time.Date(2013, 5, 15, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2022, time.September, 1, 12, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2022, time.September, 1, 12, 15, 0, 0, time.UTC), + DeletedAt: nil, + HasBranchAccess: false, + Username: "ivan101", + Password: "securepassword123", + LastGroup: backoffice2.Group{ + ID: 201, + GroupStudentID: 301, + Title: "Группа по математике", + Content: "Основы математики для младших школьников", + Track: 1, + Status: 1, + StartTime: time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 6, 30, 11, 0, 0, 0, time.UTC), + CourseID: 401, + CreatedAt: time.Date(2022, 12, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 2, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + Groups: []backoffice2.Group{ + { + ID: 201, + GroupStudentID: 301, + Title: "Группа по математике", + Content: "Основы математики для младших школьников", + Track: 1, + Status: 1, + StartTime: time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 6, 30, 11, 0, 0, 0, time.UTC), + CourseID: 401, + CreatedAt: time.Date(2022, 12, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 2, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + { + ID: 202, + GroupStudentID: 302, + Title: "Уравнения и геометрия", + Content: "Разбор уравнений и элементы геометрии", + Track: 2, + Status: 1, + StartTime: time.Date(2023, 7, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, 12, 31, 11, 0, 0, 0, time.UTC), + CourseID: 402, + CreatedAt: time.Date(2023, 6, 1, 9, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, 6, 10, 10, 0, 0, 0, time.UTC), + DeletedAt: nil, + }, + }, + }, + }, + }, +} +var groupInfoBackoffice = backoffice2.GroupInfo{ + Status: "success", + Data: backoffice2.GroupDataFull{ + ID: 101, + Title: "Основы программирования", + Content: "Изучение основ программирования для детей", + Type: backoffice2.TypeFull{ + Value: "programming", + Label: "Программирование", + Tag: "coding", + }, + Status: backoffice2.StatusFull{ + Value: 1, + Label: "Активная", + Tag: "active", + }, + StatusChangedAt: "2022-08-15T12:00:00Z", + StartTime: "2022-09-01T10:00:00Z", + NextLessonTime: "2022-09-08T10:00:00Z", + LessonsTotal: 12, + LessonsPassed: 3, + HardwareNeeded: 1, + Branch: backoffice2.BranchFull{ + ID: 1, + Title: "Отделение Центральный", + Code: "CTR001", + Description: "Центральное отделение", + Phone: "+79998887766", + Email: "info@branch.example.com", + SiteURL: "https://branch.example.com", + TemplateVersion: 1, + UseAmo: true, + AmoConfigID: 1001, + ShowFinanceInfo: true, + LmsDisplayStudentCredentials: true, + ShowOnlineRoomURLField: 1, + UseSms: true, + LanguageID: 1, + OrderName: 1, + UseFullyPaidLabel: 0, + BrandName: "Учебный Центр", + MaxCountStudentsForShowOnline: 30, + IsFillPaymentSystem: true, + FirstLessonNoRoyalty: 0, + RootBranchID: 0, + }, + Venue: backoffice2.VenueFull{ + ID: 10, + Title: "Салон №1", + Address: "ул. Большая Красная, д.1", + ContactName: "Иван Иванов", + ContactEmail: "ivan.ivanov@venue.example.com", + ContactPhone: "+79991112233", + }, + Curator: backoffice2.UserFull{ + ID: 201, + Username: "ivan_ivanov", + Phone: "+79991234567", + Email: "ivan.ivanov@curator.example.com", + Name: "Иван Иванов", + Profile: backoffice2.ProfileFull{ + PhotoURL: "https://example.com/photos/ivan_ivanov.jpg", + Promo: "Популярный куратор", + }, + Status: 1, + Links: backoffice2.LinksFull{ + Self: "https://backoffice.example.com/api/users/201", + }, + }, + Teacher: backoffice2.TeacherFull{ + ID: 301, + Username: "anna_smirnova", + Phone: "+79995556677", + Email: "anna.smirnova@teacher.example.com", + Name: "Анна Смирнова", + Profile: backoffice2.ProfileFull{ + PhotoURL: "https://example.com/photos/anna_smirnova.jpg", + Promo: "Стажированный преподаватель", + }, + AllowedUserCourses: nil, + Status: 1, + Links: backoffice2.LinksFull{ + Self: "https://backoffice.example.com/api/teachers/301", + }, + }, + Teachers: nil, + ClientManager: nil, + Course: backoffice2.CourseFull{ + ID: 401, + Name: "Основы программирования", + GUID: "COURSE001", + Description: "Изучение основ программирования для детей", + ContentType: "interactive", + CourseType: backoffice2.CourseTypeFull{ + ID: 1, + Title: "Технологии", + Code: "tech", + }, + LessonsCount: 12, + GroupLessonsAmount: 4, + LessonsCountFormatted: "12", + GroupLessonsAmountFormatted: "4", + IsDeleted: 0, + Links: backoffice2.LinksFull{ + Self: "https://backoffice.example.com/api/courses/401", + }, + }, + LanguageID: 1, + Journal: true, + ShowJournal: true, + ShowOnlineRoom: true, + IsOnline: true, + ActiveStudentCount: 25, + OnlineRoomURL: "https://online-room.example.com/group/101", + UseClientManager: 1, + DisplayLessonDurationInMinutes: 90, + DeletedAt: nil, + DeletedBy: nil, + PriorityLevel: backoffice2.PriorityLevelFull{ + Value: "high", + Label: "Высокий", + Tag: "high", + }, + IsFull: false, + CreatedAt: "2022-07-10T10:00:00Z", + CreatedBy: backoffice2.UserFull{ + ID: 202, + Username: "petr_petrov", + Phone: "+79992223344", + Email: "petr.petrov@teacher.example.com", + Name: "Пётр Петров", + Profile: backoffice2.ProfileFull{ + PhotoURL: "https://example.com/photos/petr_petrov.jpg", + Promo: "Учитель с большим опытом", + }, + Status: 1, + Links: backoffice2.LinksFull{ + Self: "https://backoffice.example.com/api/users/202", + }, + }, + Related: backoffice2.RelatedFull{ + Statuses: nil, + Types: nil, + PriorityLevels: nil, + }, + }, +} + +var expectedGroupView = models.GroupView{ + GroupID: 101, + GroupTitle: "Основы программирования", + GroupContent: "Изучение основ программирования для детей", + NextLessonTime: "2022-09-08T10:00:00Z", + LessonsTotal: 12, + LessonsPassed: 3, + ActiveKids: []models.GroupKid(nil), + NotActiveKids: []models.GroupKid{{ + ID: 1, + FullName: "Иван Иванов", + LastGroup: models.KidGroup{ + ID: 201, + StartTime: time.Date(2023, time.February, 1, 10, 0, 0, 0, time.UTC), + EndTime: time.Date(2023, time.June, 30, 11, 0, 0, 0, time.UTC), + }, + }}, +} diff --git a/test/telegram/handlers/viewInformer_test.go b/test/telegram/handlers/viewInformer_test.go index a17eec7..e05fcc7 100644 --- a/test/telegram/handlers/viewInformer_test.go +++ b/test/telegram/handlers/viewInformer_test.go @@ -61,7 +61,7 @@ func TestViewInformer(t *testing.T) { Type: domain.UserType, Data: []string{"789", "321"}, }, nil), - viewFetcher.EXPECT().KidView(int64(1), "789", "321").Return(models.KidView{ + viewFetcher.EXPECT().KidView(int64(1), "789", "321", "").Return(models.KidView{ Extra: "", Kid: models.Kid{ FullName: "Алексей Смирнов", @@ -111,7 +111,7 @@ func TestViewInformer(t *testing.T) { Type: domain.UserType, Data: []string{"789", "321"}, }, nil), - viewFetcher.EXPECT().KidView(int64(1), "789", "321").Return(models.KidView{ + viewFetcher.EXPECT().KidView(int64(1), "789", "321", "").Return(models.KidView{ Extra: models.NotAccessible, Kid: models.Kid{ FullName: "Алексей Смирнов", @@ -175,7 +175,7 @@ func TestViewInformer(t *testing.T) { Type: domain.UserType, Data: []string{"789", "123"}, }, nil), - viewFetcher.EXPECT().KidView(int64(1), "789", "123").Return(models.KidView{}, errExp).Times(1), + viewFetcher.EXPECT().KidView(int64(1), "789", "123", "").Return(models.KidView{}, errExp).Times(1), mctx.EXPECT().Send("⚠️ Невозможно получить данного ученика!").Return(nil).Times(1), ) err := handler.ServeContext(mctx) @@ -190,7 +190,7 @@ func TestViewInformer(t *testing.T) { Type: domain.GroupType, Data: []string{"123"}, }, nil), - viewFetcher.EXPECT().GroupView(int64(1), "123").Return(models.GroupView{ + viewFetcher.EXPECT().GroupView(int64(1), "123", "").Return(models.GroupView{ GroupID: 1, GroupTitle: "Математика для детей", GroupContent: "Основы арифметики и геометрии", @@ -269,7 +269,7 @@ func TestViewInformer(t *testing.T) { Type: domain.GroupType, Data: []string{"123"}, }, nil), - viewFetcher.EXPECT().GroupView(int64(1), "123").Return(models.GroupView{}, errExp).Times(1), + viewFetcher.EXPECT().GroupView(int64(1), "123", "").Return(models.GroupView{}, errExp).Times(1), mctx.EXPECT().Send("⚠️ Невозможно получить данную группу!").Return(nil).Times(1), ) err := handler.ServeContext(mctx) @@ -277,5 +277,4 @@ func TestViewInformer(t *testing.T) { }) }) }) - } From aef4cb69b508706de766a3e4bd5c4cf59dc21865 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 15:04:49 +0300 Subject: [PATCH 29/44] add backoffice methods + tests --- internal/lib/backoffice/groupView.go | 14 +- internal/lib/backoffice/kidsNamesByGroup.go | 32 + test/lib/backoffice/GroupView_example | 991 +++++++++++++++++++ test/lib/backoffice/KidsNamesByGroup_example | 40 + test/lib/backoffice/backoffice_test.go | 132 +++ 5 files changed, 1202 insertions(+), 7 deletions(-) create mode 100644 internal/lib/backoffice/kidsNamesByGroup.go create mode 100644 test/lib/backoffice/GroupView_example create mode 100644 test/lib/backoffice/KidsNamesByGroup_example diff --git a/internal/lib/backoffice/groupView.go b/internal/lib/backoffice/groupView.go index 2788c60..15d7270 100644 --- a/internal/lib/backoffice/groupView.go +++ b/internal/lib/backoffice/groupView.go @@ -1,31 +1,31 @@ package backoffice import ( - "algobot/internal/domain/models" + "algobot/internal/domain/backoffice" "encoding/json" "fmt" ) -func (bo *Backoffice) GroupView(uid int64, groupID string, cookie string) (models.GroupView, error) { +func (bo *Backoffice) GroupView(groupID string, cookie string) (backoffice.GroupInfo, error) { const op = "backoffice.GetGroupView" req, err := bo.createReq("GET", "/api/v1/group/"+groupID, cookie, map[string]string{ "expand": "venue,teacher,curator,branch", }, nil) if err != nil { - return models.GroupView{}, fmt.Errorf("%s err create req: %w", op, err) + return backoffice.GroupInfo{}, fmt.Errorf("%s err create req: %w", op, err) } data, err := bo.doReq(req) if err != nil { - return models.GroupView{}, fmt.Errorf("%s err doReq: %w", op, err) + return backoffice.GroupInfo{}, fmt.Errorf("%s err doReq: %w", op, err) } - var response models.GroupView + var response backoffice.GroupInfo err = json.NewDecoder(data.Body).Decode(&response) if err != nil { - return models.GroupView{}, fmt.Errorf("%s err decode json: %w", op, err) + return backoffice.GroupInfo{}, fmt.Errorf("%s err decode json: %w", op, err) } - return models.GroupView{}, nil + return response, nil } diff --git a/internal/lib/backoffice/kidsNamesByGroup.go b/internal/lib/backoffice/kidsNamesByGroup.go new file mode 100644 index 0000000..f21a903 --- /dev/null +++ b/internal/lib/backoffice/kidsNamesByGroup.go @@ -0,0 +1,32 @@ +package backoffice + +import ( + "algobot/internal/domain/backoffice" + "encoding/json" + "fmt" +) + +func (bo *Backoffice) KidsNamesByGroup(groupId string, cookie string) (backoffice.NamesByGroup, error) { + const op = "backoffice.KidsNamesByGroup" + + req, err := bo.createReq("GET", "/api/v2/group/student/index", cookie, map[string]string{ + "groupId": groupId, + "expand": "lastGroup, groups", + }, nil) + if err != nil { + return backoffice.NamesByGroup{}, fmt.Errorf("%s cant create req: %w", op, err) + } + + data, reqErr := bo.doReq(req) + if reqErr != nil { + return backoffice.NamesByGroup{}, fmt.Errorf("%s cant do req: %w", op, err) + } + + var response backoffice.NamesByGroup + err = json.NewDecoder(data.Body).Decode(&response) + if err != nil { + return backoffice.NamesByGroup{}, fmt.Errorf("%s cant decode res: %w", op, err) + } + + return response, nil +} diff --git a/test/lib/backoffice/GroupView_example b/test/lib/backoffice/GroupView_example new file mode 100644 index 0000000..684903d --- /dev/null +++ b/test/lib/backoffice/GroupView_example @@ -0,0 +1,991 @@ +{ + "status": "success", + "data": { + "id": 12345678, + "title": "Занятие в библиотеке 5 в 14.00", + "content": "Группа по курсу Python", + "type": { + "value": "regular", + "label": "Группа", + "tag": "default" + }, + "status": { + "value": 10, + "label": "Активная", + "tag": "success" + }, + "status_changed_at": "20.09.2024 12:04", + "start_time": "22.09.2024 14:00", + "next_lesson_time": "13.04.2025 14:00", + "lessons_total": 35, + "lessons_passed": 28, + "hardware_needed": 0, + "branch": { + "id": 987, + "title": "Город Х", + "code": "city_x", + "description": "", + "phone": "+7 (491) 555-22-33", + "email": "cityx@algoritmika.org", + "site_url": "https://cityx.algoritmika.org", + "templateVersion": 2, + "use_amo": true, + "amoConfigId": 321, + "show_finance_info": true, + "lms_display_student_credentials": true, + "show_online_room_url_field": 0, + "use_sms": false, + "language_id": 2, + "order_name": 1, + "use_fully_paid_label": 0, + "brandName": "", + "max_count_students_for_show_online": 10, + "isFillPaymentSystem": false, + "firstLessonNoRoyalty": 0, + "root_branch_id": 123 + }, + "venue": { + "id": 4321, + "title": "Библиотека №5", + "address": "390000, Город Х, ул Ленина, д 10", + "contact_name": "", + "contact_email": "", + "contact_phone": "", + "_links": { + "self": "/venue/view/4321", + "update": "/venue/update/4321", + "index": "/venue" + } + }, + "curator": { + "id": 1234, + "username": "random_user", + "phone": "+7 (900) 555-12-34", + "email": "example123@mail.com", + "name": "Иван Иванов", + "profile": { + "photo_url": "/uploads/avatar/avatar/avatar_1234_1603094230-96x96.jpg", + "promo": "" + }, + "allowedUserCourses": [], + "status": 10, + "_links": { + "self": "/user/update/1234" + } + }, + "teacher": { + "id": 5678, + "username": "random_user2", + "phone": "+7 (922) 555-00-11", + "email": "randomuser2@mail.com", + "name": "Алексей Смирнов", + "profile": { + "photo_url": "", + "promo": null + }, + "allowedUserCourses": [ + { + "userId": 42407, + "courseId": 84, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 305, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 389, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 405, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 406, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 407, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 408, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 416, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 417, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 448, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 465, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 606, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 640, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 641, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 661, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 662, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 665, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 666, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 686, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 706, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 707, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 716, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 727, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 729, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 734, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 735, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 777, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 783, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 797, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 799, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 809, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 810, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 823, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 831, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 852, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 853, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 857, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 858, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 859, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 860, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 861, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 862, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 864, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1338, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1339, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1346, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1347, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1387, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1459, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1484, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1543, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1554, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1613, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1614, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1615, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1616, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1653, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1654, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1664, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1665, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1668, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1686, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1688, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1692, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1710, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1748, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1767, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1810, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1904, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2007, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2033, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2125, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2126, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2231, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2259, + "isAllowed": 1 + } + ], + "status": 10, + "_links": { + "self": "/user/update/42407" + } + }, + "teachers": [ + { + "id": 5678, + "username": "random_user2", + "phone": "+7 (922) 555-00-11", + "email": "randomuser2@mail.com", + "name": "Алексей Смирнов", + "profile": { + "photo_url": "", + "promo": null + }, + "allowedUserCourses": [ + { + "userId": 42407, + "courseId": 84, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 305, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 389, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 405, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 406, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 407, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 408, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 416, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 417, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 448, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 465, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 606, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 640, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 641, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 661, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 662, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 665, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 666, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 686, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 706, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 707, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 716, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 727, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 729, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 734, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 735, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 777, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 783, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 797, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 799, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 809, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 810, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 823, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 831, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 852, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 853, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 857, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 858, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 859, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 860, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 861, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 862, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 864, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1338, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1339, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1346, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1347, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1387, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1459, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1484, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1543, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1554, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1613, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1614, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1615, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1616, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1653, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1654, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1664, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1665, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1668, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1686, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1688, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1692, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1710, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1748, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1767, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1810, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 1904, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2007, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2033, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2125, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2126, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2231, + "isAllowed": 1 + }, + { + "userId": 42407, + "courseId": 2259, + "isAllowed": 1 + } + ], + "status": 10, + "_links": { + "self": "/user/update/42407" + } + } + ], + "client_manager": null, + "course": { + "id": 729, + "name": "Компьютерная грамотность", + "guid": "a131029b-cde5-11eb-a724-6cb31107bf10", + "description": "Курс для внеурочных занятий (дополнительное образование) с детьми в возрасте 7-9 лет, на русском языке, версия 2021/2022", + "contentType": "course", + "courseType": { + "id": 19, + "title": "компьютерная грамотность", + "code": "comp" + }, + "lessons_count": 0, + "group_lessons_amount": 0, + "lessons_count_formatted": "нет модулей", + "group_lessons_amount_formatted": "нет уроков", + "is_deleted": 0, + "_links": { + "self": "/course/view/a131029b-cde5-11eb-a724-6cb31107bf10" + } + }, + "language_id": null, + "journal": true, + "show_journal": true, + "showOnlineRoom": true, + "isOnline": false, + "active_student_count": 8, + "online_room_url": "", + "use_client_manager": 0, + "display_lesson_duration_in_minutes": 60, + "deleted_at": null, + "deleted_by": null, + "priority_level": { + "value": "normal", + "label": "Обычный приоритет", + "tag": "default" + }, + "is_full": false, + "created_at": "17.09.2024 10:41", + "created_by": { + "id": 1234, + "username": "random_user", + "phone": "+7 (900) 555-12-34", + "email": "example123@mail.com", + "name": "Иван Иванов", + "profile": { + "photo_url": "/uploads/avatar/avatar/avatar_1234_1603094230-96x96.jpg", + "promo": "" + }, + "allowedUserCourses": [], + "status": 10, + "_links": { + "self": "/user/update/1234" + } + }, + "_related": { + "statuses": [ + { + "value": 10, + "label": "Активная", + "tag": "success" + }, + { + "value": 1, + "label": "Не стартовала", + "tag": "warning" + }, + { + "value": 20, + "label": "Идет набор", + "tag": "warning" + }, + { + "value": 30, + "label": "Приостановлена", + "tag": "warning" + }, + { + "value": 0, + "label": "Окончена", + "tag": "default" + }, + { + "value": 2, + "label": "Развалилась", + "tag": "default" + } + ], + "types": [ + { + "value": "regular", + "label": "Группа", + "tag": "default" + }, + { + "value": "masterclass", + "label": "Мастер-класс", + "tag": "info" + }, + { + "value": "intensive", + "label": "Интенсив", + "tag": "warning" + }, + { + "value": "demo", + "label": "Обучение сотрудников", + "tag": "inactive" + }, + { + "value": "individual", + "label": "Индивидуальная", + "tag": "default" + } + ], + "priority_levels": [ + { + "value": "normal", + "label": "Обычный", + "tag": "default" + }, + { + "value": "high", + "label": "Высокий", + "tag": "warning" + } + ] + } + } +} \ No newline at end of file diff --git a/test/lib/backoffice/KidsNamesByGroup_example b/test/lib/backoffice/KidsNamesByGroup_example new file mode 100644 index 0000000..aab79ff --- /dev/null +++ b/test/lib/backoffice/KidsNamesByGroup_example @@ -0,0 +1,40 @@ +{ + "status": "success", + "data": { + "items": [ + { + "id": 70245813, + "firstName": "Иван", + "lastName": "Петров", + "fullName": "Иван Петров", + "parentName": "Ольга", + "email": "petrov_ivan@mail.ru", + "hasLaptop": -1, + "phone": "+7 (915) 123-45-67", + "age": 10, + "birthDate": "2014-12-16T00:00:00+03:00", + "deletedAt": null, + "hasBranchAccess": true, + "username": "petrov_i", + "password": "7605", + "lastGroup": { + "id": 98637162, + "groupStudentId": 6553709, + "title": "Библиотека 7 вс 14.00", + "content": "Группа по курсу КГ", + "track": 2, + "status": 0, + "startTime": "2024-11-25T10:54:55+03:00", + "endTime": "9999-12-31T00:00:00+03:00", + "courseId": 729, + "deletedAt": null + }, + "_links": { + "self": { + "href": "/student/update/70245813" + } + } + } + ] + } +} \ No newline at end of file diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go index ed95df7..313ee82 100644 --- a/test/lib/backoffice/backoffice_test.go +++ b/test/lib/backoffice/backoffice_test.go @@ -2,6 +2,7 @@ package backoffice import ( "algobot/internal/config" + backoffice2 "algobot/internal/domain/backoffice" "algobot/internal/domain/models" "algobot/internal/lib/backoffice" "github.com/stretchr/testify/assert" @@ -45,6 +46,42 @@ func TestBackoffice(t *testing.T) { assert.NoError(t, err) assert.Equal(t, groupsExp, group) }) + t.Run("GroupView", func(t *testing.T) { + responseHTML := readFile("GroupView_example") + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + group, err := bo.GroupView("", "") + assert.NoError(t, err) + assert.Equal(t, backofficeGroupViewExpected, group) + }) + t.Run("KidsNamesByGroup", func(t *testing.T) { + responseHTML := readFile("KidsNamesByGroup_example") + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + group, err := bo.KidsNamesByGroup("", "") + assert.NoError(t, err) + assert.Equal(t, backofficeKidsByGroupExpected, group) + }) } func readFile(fileName string) string { @@ -59,3 +96,98 @@ func readFile(fileName string) string { responseHTML := string(b) return responseHTML } + +var backofficeGroupViewExpected = backoffice2.GroupInfo{ + Status: "success", + Data: backoffice2.GroupDataFull{ID: 12345678, Title: "Занятие в библиотеке 5 в 14.00", Content: "Группа по курсу Python", Type: backoffice2.TypeFull{Value: "regular", Label: "Группа", Tag: "default"}, Status: backoffice2.StatusFull{Value: 10, Label: "Активная", Tag: "success"}, StatusChangedAt: "20.09.2024 12:04", StartTime: "22.09.2024 14:00", NextLessonTime: "13.04.2025 14:00", LessonsTotal: 35, LessonsPassed: 28, HardwareNeeded: 0, Branch: backoffice2.BranchFull{ + ID: 987, + Title: "Город Х", + Code: "city_x", + Description: "", + Phone: "+7 (491) 555-22-33", + Email: "cityx@algoritmika.org", + SiteURL: "https://cityx.algoritmika.org", + TemplateVersion: 2, + UseAmo: true, + AmoConfigID: 321, + ShowFinanceInfo: true, + LmsDisplayStudentCredentials: true, + ShowOnlineRoomURLField: 0, + UseSms: false, + LanguageID: 2, + OrderName: 1, + UseFullyPaidLabel: 0, + BrandName: "", + MaxCountStudentsForShowOnline: 10, + IsFillPaymentSystem: false, + FirstLessonNoRoyalty: 0, + RootBranchID: 123, + }, Venue: backoffice2.VenueFull{ID: 4321, Title: "Библиотека №5", Address: "390000, Город Х, ул Ленина, д 10", ContactName: "", ContactEmail: "", ContactPhone: "", Links: backoffice2.LinksFull{Self: "/venue/view/4321"}}, Curator: backoffice2.UserFull{ID: 1234, Username: "random_user", Phone: "+7 (900) 555-12-34", Email: "example123@mail.com", Name: "Иван Иванов", Profile: backoffice2.ProfileFull{PhotoURL: "/uploads/avatar/avatar/avatar_1234_1603094230-96x96.jpg", Promo: ""}, Status: 10, Links: backoffice2.LinksFull{Self: "/user/update/1234"}}, Teacher: backoffice2.TeacherFull{ID: 5678, Username: "random_user2", Phone: "+7 (922) 555-00-11", Email: "randomuser2@mail.com", Name: "Алексей Смирнов", Profile: backoffice2.ProfileFull{PhotoURL: "", Promo: ""}, AllowedUserCourses: []backoffice2.AllowedUserCourseFull{{UserID: 42407, CourseID: 84, IsAllowed: 1}, {UserID: 42407, CourseID: 305, IsAllowed: 1}, {UserID: 42407, CourseID: 389, IsAllowed: 1}, {UserID: 42407, CourseID: 405, IsAllowed: 1}, {UserID: 42407, CourseID: 406, IsAllowed: 1}, {UserID: 42407, CourseID: 407, IsAllowed: 1}, {UserID: 42407, CourseID: 408, IsAllowed: 1}, {UserID: 42407, CourseID: 416, IsAllowed: 1}, {UserID: 42407, CourseID: 417, IsAllowed: 1}, {UserID: 42407, CourseID: 448, IsAllowed: 1}, {UserID: 42407, CourseID: 465, IsAllowed: 1}, {UserID: 42407, CourseID: 606, IsAllowed: 1}, {UserID: 42407, CourseID: 640, IsAllowed: 1}, {UserID: 42407, CourseID: 641, IsAllowed: 1}, {UserID: 42407, CourseID: 661, IsAllowed: 1}, {UserID: 42407, CourseID: 662, IsAllowed: 1}, {UserID: 42407, CourseID: 665, IsAllowed: 1}, {UserID: 42407, CourseID: 666, IsAllowed: 1}, {UserID: 42407, CourseID: 686, IsAllowed: 1}, {UserID: 42407, CourseID: 706, IsAllowed: 1}, {UserID: 42407, CourseID: 707, IsAllowed: 1}, {UserID: 42407, CourseID: 716, IsAllowed: 1}, {UserID: 42407, CourseID: 727, IsAllowed: 1}, {UserID: 42407, CourseID: 729, IsAllowed: 1}, {UserID: 42407, CourseID: 734, IsAllowed: 1}, {UserID: 42407, CourseID: 735, IsAllowed: 1}, {UserID: 42407, CourseID: 777, IsAllowed: 1}, {UserID: 42407, CourseID: 783, IsAllowed: 1}, {UserID: 42407, CourseID: 797, IsAllowed: 1}, {UserID: 42407, CourseID: 799, IsAllowed: 1}, {UserID: 42407, CourseID: 809, IsAllowed: 1}, {UserID: 42407, CourseID: 810, IsAllowed: 1}, {UserID: 42407, CourseID: 823, IsAllowed: 1}, {UserID: 42407, CourseID: 831, IsAllowed: 1}, {UserID: 42407, CourseID: 852, IsAllowed: 1}, {UserID: 42407, CourseID: 853, IsAllowed: 1}, {UserID: 42407, CourseID: 857, IsAllowed: 1}, {UserID: 42407, CourseID: 858, IsAllowed: 1}, {UserID: 42407, CourseID: 859, IsAllowed: 1}, {UserID: 42407, CourseID: 860, IsAllowed: 1}, {UserID: 42407, CourseID: 861, IsAllowed: 1}, {UserID: 42407, CourseID: 862, IsAllowed: 1}, {UserID: 42407, CourseID: 864, IsAllowed: 1}, {UserID: 42407, CourseID: 1338, IsAllowed: 1}, {UserID: 42407, CourseID: 1339, IsAllowed: 1}, {UserID: 42407, CourseID: 1346, IsAllowed: 1}, {UserID: 42407, CourseID: 1347, IsAllowed: 1}, {UserID: 42407, CourseID: 1387, IsAllowed: 1}, {UserID: 42407, CourseID: 1459, IsAllowed: 1}, {UserID: 42407, CourseID: 1484, IsAllowed: 1}, {UserID: 42407, CourseID: 1543, IsAllowed: 1}, {UserID: 42407, CourseID: 1554, IsAllowed: 1}, {UserID: 42407, CourseID: 1613, IsAllowed: 1}, {UserID: 42407, CourseID: 1614, IsAllowed: 1}, {UserID: 42407, CourseID: 1615, IsAllowed: 1}, {UserID: 42407, CourseID: 1616, IsAllowed: 1}, {UserID: 42407, CourseID: 1653, IsAllowed: 1}, {UserID: 42407, CourseID: 1654, IsAllowed: 1}, {UserID: 42407, CourseID: 1664, IsAllowed: 1}, {UserID: 42407, CourseID: 1665, IsAllowed: 1}, {UserID: 42407, CourseID: 1668, IsAllowed: 1}, {UserID: 42407, CourseID: 1686, IsAllowed: 1}, {UserID: 42407, CourseID: 1688, IsAllowed: 1}, {UserID: 42407, CourseID: 1692, IsAllowed: 1}, {UserID: 42407, CourseID: 1710, IsAllowed: 1}, {UserID: 42407, CourseID: 1748, IsAllowed: 1}, {UserID: 42407, CourseID: 1767, IsAllowed: 1}, {UserID: 42407, CourseID: 1810, IsAllowed: 1}, {UserID: 42407, CourseID: 1904, IsAllowed: 1}, {UserID: 42407, CourseID: 2007, IsAllowed: 1}, {UserID: 42407, CourseID: 2033, IsAllowed: 1}, {UserID: 42407, CourseID: 2125, IsAllowed: 1}, {UserID: 42407, CourseID: 2126, IsAllowed: 1}, {UserID: 42407, CourseID: 2231, IsAllowed: 1}, {UserID: 42407, CourseID: 2259, IsAllowed: 1}}, Status: 10, Links: backoffice2.LinksFull{Self: "/user/update/42407"}}, Teachers: []backoffice2.TeacherFull{{ + ID: 5678, + Username: "random_user2", + Phone: "+7 (922) 555-00-11", + Email: "randomuser2@mail.com", + Name: "Алексей Смирнов", + Profile: backoffice2.ProfileFull{PhotoURL: "", Promo: ""}, + AllowedUserCourses: []backoffice2.AllowedUserCourseFull{{UserID: 42407, CourseID: 84, IsAllowed: 1}, {UserID: 42407, CourseID: 305, IsAllowed: 1}, {UserID: 42407, CourseID: 389, IsAllowed: 1}, {UserID: 42407, CourseID: 405, IsAllowed: 1}, {UserID: 42407, CourseID: 406, IsAllowed: 1}, {UserID: 42407, CourseID: 407, IsAllowed: 1}, {UserID: 42407, CourseID: 408, IsAllowed: 1}, {UserID: 42407, CourseID: 416, IsAllowed: 1}, {UserID: 42407, CourseID: 417, IsAllowed: 1}, {UserID: 42407, CourseID: 448, IsAllowed: 1}, {UserID: 42407, CourseID: 465, IsAllowed: 1}, {UserID: 42407, CourseID: 606, IsAllowed: 1}, {UserID: 42407, CourseID: 640, IsAllowed: 1}, {UserID: 42407, CourseID: 641, IsAllowed: 1}, {UserID: 42407, CourseID: 661, IsAllowed: 1}, {UserID: 42407, CourseID: 662, IsAllowed: 1}, {UserID: 42407, CourseID: 665, IsAllowed: 1}, {UserID: 42407, CourseID: 666, IsAllowed: 1}, {UserID: 42407, CourseID: 686, IsAllowed: 1}, {UserID: 42407, CourseID: 706, IsAllowed: 1}, {UserID: 42407, CourseID: 707, IsAllowed: 1}, {UserID: 42407, CourseID: 716, IsAllowed: 1}, {UserID: 42407, CourseID: 727, IsAllowed: 1}, {UserID: 42407, CourseID: 729, IsAllowed: 1}, {UserID: 42407, CourseID: 734, IsAllowed: 1}, {UserID: 42407, CourseID: 735, IsAllowed: 1}, {UserID: 42407, CourseID: 777, IsAllowed: 1}, {UserID: 42407, CourseID: 783, IsAllowed: 1}, {UserID: 42407, CourseID: 797, IsAllowed: 1}, {UserID: 42407, CourseID: 799, IsAllowed: 1}, {UserID: 42407, CourseID: 809, IsAllowed: 1}, {UserID: 42407, CourseID: 810, IsAllowed: 1}, {UserID: 42407, CourseID: 823, IsAllowed: 1}, {UserID: 42407, CourseID: 831, IsAllowed: 1}, {UserID: 42407, CourseID: 852, IsAllowed: 1}, {UserID: 42407, CourseID: 853, IsAllowed: 1}, {UserID: 42407, CourseID: 857, IsAllowed: 1}, {UserID: 42407, CourseID: 858, IsAllowed: 1}, {UserID: 42407, CourseID: 859, IsAllowed: 1}, {UserID: 42407, CourseID: 860, IsAllowed: 1}, {UserID: 42407, CourseID: 861, IsAllowed: 1}, {UserID: 42407, CourseID: 862, IsAllowed: 1}, {UserID: 42407, CourseID: 864, IsAllowed: 1}, {UserID: 42407, CourseID: 1338, IsAllowed: 1}, {UserID: 42407, CourseID: 1339, IsAllowed: 1}, {UserID: 42407, CourseID: 1346, IsAllowed: 1}, {UserID: 42407, CourseID: 1347, IsAllowed: 1}, {UserID: 42407, CourseID: 1387, IsAllowed: 1}, {UserID: 42407, CourseID: 1459, IsAllowed: 1}, {UserID: 42407, CourseID: 1484, IsAllowed: 1}, {UserID: 42407, CourseID: 1543, IsAllowed: 1}, {UserID: 42407, CourseID: 1554, IsAllowed: 1}, {UserID: 42407, CourseID: 1613, IsAllowed: 1}, {UserID: 42407, CourseID: 1614, IsAllowed: 1}, {UserID: 42407, CourseID: 1615, IsAllowed: 1}, {UserID: 42407, CourseID: 1616, IsAllowed: 1}, {UserID: 42407, CourseID: 1653, IsAllowed: 1}, {UserID: 42407, CourseID: 1654, IsAllowed: 1}, {UserID: 42407, CourseID: 1664, IsAllowed: 1}, {UserID: 42407, CourseID: 1665, IsAllowed: 1}, {UserID: 42407, CourseID: 1668, IsAllowed: 1}, {UserID: 42407, CourseID: 1686, IsAllowed: 1}, {UserID: 42407, CourseID: 1688, IsAllowed: 1}, {UserID: 42407, CourseID: 1692, IsAllowed: 1}, {UserID: 42407, CourseID: 1710, IsAllowed: 1}, {UserID: 42407, CourseID: 1748, IsAllowed: 1}, {UserID: 42407, CourseID: 1767, IsAllowed: 1}, {UserID: 42407, CourseID: 1810, IsAllowed: 1}, {UserID: 42407, CourseID: 1904, IsAllowed: 1}, {UserID: 42407, CourseID: 2007, IsAllowed: 1}, {UserID: 42407, CourseID: 2033, IsAllowed: 1}, {UserID: 42407, CourseID: 2125, IsAllowed: 1}, {UserID: 42407, CourseID: 2126, IsAllowed: 1}, {UserID: 42407, CourseID: 2231, IsAllowed: 1}, {UserID: 42407, CourseID: 2259, IsAllowed: 1}}, + Status: 10, + Links: backoffice2.LinksFull{Self: "/user/update/42407"}, + }}, ClientManager: interface{}(nil), Course: backoffice2.CourseFull{ + ID: 729, + Name: "Компьютерная грамотность", + GUID: "a131029b-cde5-11eb-a724-6cb31107bf10", + Description: "Курс для внеурочных занятий (дополнительное образование) с детьми в возрасте 7-9 лет, на русском языке, версия 2021/2022", + ContentType: "course", + CourseType: backoffice2.CourseTypeFull{ID: 19, Title: "компьютерная грамотность", Code: "comp"}, + LessonsCount: 0, + GroupLessonsAmount: 0, + LessonsCountFormatted: "нет модулей", + GroupLessonsAmountFormatted: "нет уроков", + IsDeleted: 0, + Links: backoffice2.LinksFull{Self: "/course/view/a131029b-cde5-11eb-a724-6cb31107bf10"}, + }, LanguageID: interface{}(nil), Journal: true, ShowJournal: true, ShowOnlineRoom: true, IsOnline: false, ActiveStudentCount: 8, OnlineRoomURL: "", UseClientManager: 0, DisplayLessonDurationInMinutes: 60, DeletedAt: interface{}(nil), DeletedBy: interface{}(nil), PriorityLevel: backoffice2.PriorityLevelFull{Value: "normal", Label: "Обычный приоритет", Tag: "default"}, IsFull: false, CreatedAt: "17.09.2024 10:41", CreatedBy: backoffice2.UserFull{ + ID: 1234, + Username: "random_user", + Phone: "+7 (900) 555-12-34", + Email: "example123@mail.com", + Name: "Иван Иванов", + Profile: backoffice2.ProfileFull{PhotoURL: "/uploads/avatar/avatar/avatar_1234_1603094230-96x96.jpg", Promo: ""}, + Status: 10, + Links: backoffice2.LinksFull{Self: "/user/update/1234"}, + }, Related: backoffice2.RelatedFull{ + Statuses: []backoffice2.StatusFull{{Value: 10, Label: "Активная", Tag: "success"}, {Value: 1, Label: "Не стартовала", Tag: "warning"}, {Value: 20, Label: "Идет набор", Tag: "warning"}, {Value: 30, Label: "Приостановлена", Tag: "warning"}, {Value: 0, Label: "Окончена", Tag: "default"}, {Value: 2, Label: "Развалилась", Tag: "default"}}, + Types: []backoffice2.TypeFull{{Value: "regular", Label: "Группа", Tag: "default"}, {Value: "masterclass", Label: "Мастер-класс", Tag: "info"}, {Value: "intensive", Label: "Интенсив", Tag: "warning"}, {Value: "demo", Label: "Обучение сотрудников", Tag: "inactive"}, {Value: "individual", Label: "Индивидуальная", Tag: "default"}}, + PriorityLevels: []backoffice2.PriorityLevelFull{{Value: "normal", Label: "Обычный", Tag: "default"}, {Value: "high", Label: "Высокий", Tag: "warning"}}, + }}, +} +var backofficeKidsByGroupExpected = backoffice2.NamesByGroup{ + Status: "success", + Data: backoffice2.GroupData{Items: []backoffice2.Student{{ + ID: 70245813, + FirstName: "Иван", + LastName: "Петров", + FullName: "Иван Петров", + ParentName: "Ольга", + Email: "petrov_ivan@mail.ru", + HasLaptop: -1, + Phone: "+7 (915) 123-45-67", + Age: 10, + BirthDate: time.Date(2014, time.December, 16, 0, 0, 0, 0, time.Local), + HasBranchAccess: true, + Username: "petrov_i", + Password: "7605", + LastGroup: backoffice2.Group{ + ID: 98637162, + GroupStudentID: 6553709, + Title: "Библиотека 7 вс 14.00", + Content: "Группа по курсу КГ", + Track: 2, + Status: 0, + StartTime: time.Date(2024, time.November, 25, 10, 54, 55, 0, time.Local), + EndTime: time.Date(9999, time.December, 31, 0, 0, 0, 0, time.Local), + CourseID: 729, + }, + Groups: []backoffice2.Group(nil), + Links: backoffice2.Links{Self: backoffice2.SelfLink{Href: "/student/update/70245813"}}, + }}}, +} From 9f7118d67ca5bfd546185114deb5cd0ec323a953 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 15:16:42 +0300 Subject: [PATCH 30/44] add backoffice KidView method + tests --- internal/app/app.go | 9 +--- internal/app/telegram/app.go | 36 +++++++-------- internal/lib/backoffice/kidView.go | 31 +++++++++++++ test/lib/backoffice/KidView_example | 50 +++++++++++++++++++++ test/lib/backoffice/backoffice_test.go | 62 ++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 internal/lib/backoffice/kidView.go create mode 100644 test/lib/backoffice/KidView_example diff --git a/internal/app/app.go b/internal/app/app.go index b447059..009b69f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -24,14 +24,7 @@ func New(log *slog.Logger, cfg *config.Config) *App { botApplication := telegram.New( log, - cfg.TelegramToken, - storage, - storage, - storage, - storage, - storage, - cfg.RateLimit, - cfg.GRPC, + cfg, storage, bo, ) diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index cae6499..889c408 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -2,12 +2,15 @@ package telegram import ( "algobot/internal/config" + backoffice3 "algobot/internal/lib/backoffice" "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" "algobot/internal/lib/logger/sl" "algobot/internal/lib/serdes/base62" + "algobot/internal/services/backoffice" "algobot/internal/services/groups" grpc2 "algobot/internal/services/grpc" + "algobot/internal/storage/sqlite" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" "algobot/internal/telegram/middleware/auth" @@ -32,17 +35,9 @@ type App struct { func New( log *slog.Logger, - token string, - grGetter groups.GroupGetter, - auther auth.Auther, - set text.UserInformer, - cookieSetter text.CookieSetter, - notifChanger callback.NotificationChanger, - rateCfg config.RateLimit, - grpcCfg config.GRPC, - dSetter groups.DomainSetter, - gFetcher groups.GroupFetcher, - + cfg *config.Config, + storage *sqlite.Sqlite, + bo *backoffice3.Backoffice, ) *App { const op = "telegram.New" @@ -51,7 +46,7 @@ func New( ) pref := tele.Settings{ - Token: token, + Token: cfg.TelegramToken, Poller: &tele.LongPoller{ Timeout: 10 * time.Second, }, @@ -68,21 +63,22 @@ func New( } // dependencies - groupServ := groups.NewGroup(log, grGetter, dSetter, gFetcher) + groupServ := groups.NewGroup(log, storage, storage, bo) stateMachine := memory.New() serdes := base62.NewSerdes(log) grpc := grpc2.NewAIService( - grpcCfg, + cfg.GRPC, grpc2.WithLogger(log), ) + boSvc := backoffice.NewBackoffice(log, storage, bo, bo) // initialize routes b.Use(trace.New(log)) b.Use(middleware.AutoRespond()) b.Use(middleware.Recover()) b.Use(logger.New(log)) - b.Use(auth.New(auther, log)) - b.Use(rate.New(log, rateCfg)) + b.Use(auth.New(storage, log)) + b.Use(rate.New(log, cfg.RateLimit)) // create routing r := router.NewRouter() @@ -91,15 +87,15 @@ func New( // message r.HandleFuncText("/start", text.NewStart(stateMachine)) - r.HandleFuncText("Настройки", text.NewSettings(set, log)) + r.HandleFuncText("Настройки", text.NewSettings(storage, log)) r.HandleFuncText("AI 🔹", text.NewAI(grpc, log, stateMachine)) r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) - r.HandleFuncRegexpText(regexp.MustCompile(`^(?m)\/start\s(.+)$`), text.NewSettings(set, log)) + r.HandleRegexpText(regexp.MustCompile(`^(?m)\/start\s(.+)$`), text.NewViewInformer(serdes, boSvc, log, b.Me.Username)) // callbacks r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) - r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(notifChanger, log)) + r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(storage, log)) r.HandleFuncCallback("\frefresh_groups", callback.RefreshGroup(groupServ, log)) }) @@ -108,7 +104,7 @@ func New( // message r.HandleFuncText("⬅️ Назад", text.NewStart(stateMachine)) - r.HandleFuncRegexpText(regexp.MustCompile(".+"), text.NewSendingCookie(log, cookieSetter, stateMachine)) + r.HandleFuncRegexpText(regexp.MustCompile(".+"), text.NewSendingCookie(log, storage, stateMachine)) }) r.Group(func(r router.Router) { // Routes for ChattingAI state diff --git a/internal/lib/backoffice/kidView.go b/internal/lib/backoffice/kidView.go new file mode 100644 index 0000000..fe4382a --- /dev/null +++ b/internal/lib/backoffice/kidView.go @@ -0,0 +1,31 @@ +package backoffice + +import ( + backoffice2 "algobot/internal/domain/backoffice" + "encoding/json" + "fmt" +) + +func (bo *Backoffice) KidView(kidID string, cookie string) (backoffice2.KidView, error) { + const op = "backoffice.KidView" + + req, err := bo.createReq("GET", "/api/v2/student/default/view/"+kidID, cookie, map[string]string{ + "expand": "groups", + }, nil) + + if err != nil { + return backoffice2.KidView{}, fmt.Errorf("%s createReq err: %w", op, err) + } + data, err := bo.doReq(req) + if err != nil { + return backoffice2.KidView{}, fmt.Errorf("%s do req err: %w", op, err) + } + + var response backoffice2.KidView + err = json.NewDecoder(data.Body).Decode(&response) + if err != nil { + return backoffice2.KidView{}, fmt.Errorf("%s decode res err: %w", op, err) + } + + return response, nil +} diff --git a/test/lib/backoffice/KidView_example b/test/lib/backoffice/KidView_example new file mode 100644 index 0000000..af1a94a --- /dev/null +++ b/test/lib/backoffice/KidView_example @@ -0,0 +1,50 @@ +{ + "status": "success", + "data": { + "id": 70245813, + "firstName": "Иван", + "lastName": "Петров", + "fullName": "Иван Петров", + "parentName": "Ольга", + "email": "student123@example.com", + "hasLaptop": -1, + "phone": "+7 (900) 123-45-67", + "age": 10, + "birthDate": "2014-12-16T00:00:00+03:00", + "deletedAt": null, + "hasBranchAccess": true, + "username": "student_01", + "password": "7605", + "groups": [ + { + "id": 98637162, + "groupStudentId": 6543284, + "title": "Библиотека 7 вс 14.00", + "content": "Группа по курсу КГ", + "track": 1, + "status": 20, + "startTime": "2024-11-18T11:18:45+03:00", + "endTime": "2024-11-23T16:56:45+03:00", + "courseId": 729, + "deletedAt": null + }, + { + "id": 98637162, + "groupStudentId": 6553709, + "title": "Библиотека 7 вс 14.00", + "content": "Группа по курсу КГ", + "track": 2, + "status": 0, + "startTime": "2024-11-25T10:54:55+03:00", + "endTime": "9999-12-31T00:00:00+03:00", + "courseId": 729, + "deletedAt": null + } + ], + "_links": { + "self": { + "href": "/student/update/70245813" + } + } + } +} \ No newline at end of file diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go index 313ee82..0b417a1 100644 --- a/test/lib/backoffice/backoffice_test.go +++ b/test/lib/backoffice/backoffice_test.go @@ -82,6 +82,24 @@ func TestBackoffice(t *testing.T) { assert.NoError(t, err) assert.Equal(t, backofficeKidsByGroupExpected, group) }) + t.Run("KidView", func(t *testing.T) { + responseHTML := readFile("KidView_example") + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + group, err := bo.KidView("", "") + assert.NoError(t, err) + assert.Equal(t, backofficeKidViewExpected, group) + }) } func readFile(fileName string) string { @@ -191,3 +209,47 @@ var backofficeKidsByGroupExpected = backoffice2.NamesByGroup{ Links: backoffice2.Links{Self: backoffice2.SelfLink{Href: "/student/update/70245813"}}, }}}, } +var backofficeKidViewExpected = backoffice2.KidView{ + Status: "success", + Data: backoffice2.Student{ + ID: 70245813, + FirstName: "Иван", + LastName: "Петров", + FullName: "Иван Петров", + ParentName: "Ольга", + Email: "student123@example.com", + HasLaptop: -1, + Phone: "+7 (900) 123-45-67", + Age: 10, + BirthDate: time.Date(2014, time.December, 16, 0, 0, 0, 0, time.Local), + DeletedAt: interface{}(nil), + HasBranchAccess: true, + Username: "student_01", + Password: "7605", + LastGroup: backoffice2.Group{ + ID: 0, + GroupStudentID: 0, + Title: "", + Content: "", + Track: 0, + Status: 0, + StartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + EndTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + CourseID: 0, + DeletedAt: interface{}(nil), + }, + Groups: []backoffice2.Group{{ + ID: 98637162, + GroupStudentID: 6543284, + Title: "Библиотека 7 вс 14.00", + Content: "Группа по курсу КГ", + Track: 1, + Status: 20, + StartTime: time.Date(2024, time.November, 18, 11, 18, 45, 0, time.Local), + EndTime: time.Date(2024, time.November, 23, 16, 56, 45, 0, time.Local), + CourseID: 729, + DeletedAt: interface{}(nil), + }, {ID: 98637162, GroupStudentID: 6553709, Title: "Библиотека 7 вс 14.00", Content: "Группа по курсу КГ", Track: 2, Status: 0, StartTime: time.Date(2024, time.November, 25, 10, 54, 55, 0, time.Local), EndTime: time.Date(9999, time.December, 31, 0, 0, 0, 0, time.Local), CourseID: 729, CreatedAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt: interface{}(nil)}}, + Links: backoffice2.Links{Self: backoffice2.SelfLink{Href: "/student/update/70245813"}}, + }, +} From cf0017599dcaedbf809a856712ff2d22a1b3a9ca Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 15:22:39 +0300 Subject: [PATCH 31/44] add fet with payload in router --- internal/telegram/handlers/text/myGroups.go | 9 +++++++-- test/telegram/handlers/myGroups_test.go | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/telegram/handlers/text/myGroups.go b/internal/telegram/handlers/text/myGroups.go index b673809..ffd47f9 100644 --- a/internal/telegram/handlers/text/myGroups.go +++ b/internal/telegram/handlers/text/myGroups.go @@ -1,12 +1,14 @@ package text import ( + "algobot/internal/domain" "algobot/internal/domain/models" "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/logger/sl" "fmt" "gopkg.in/telebot.v4" "log/slog" + "strconv" "strings" "time" ) @@ -26,7 +28,7 @@ type Grouper interface { } type GroupSerializer interface { - Serialize(group models.Group, traceID interface{}) (string, error) + Serialize(msg domain.SerializeMessage) (string, error) } type MyGroup struct { log *slog.Logger @@ -99,7 +101,10 @@ func (g *MyGroup) getFormattedTitle(group models.Group, ctx telebot.Context) str slog.Any("trace_id", ctx.Get("trace_id")), ) - serialized, err := g.serializer.Serialize(group, ctx.Get("trace_id")) + serialized, err := g.serializer.Serialize(domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{strconv.Itoa(group.GroupID)}, + }) if err != nil { log.Warn("error while serializing group", sl.Err(err)) return group.Title diff --git a/test/telegram/handlers/myGroups_test.go b/test/telegram/handlers/myGroups_test.go index 2cf8426..66bd619 100644 --- a/test/telegram/handlers/myGroups_test.go +++ b/test/telegram/handlers/myGroups_test.go @@ -1,6 +1,7 @@ package test import ( + "algobot/internal/domain" "algobot/internal/domain/models" "algobot/internal/domain/telegram/keyboards" "algobot/internal/telegram/handlers/text" @@ -11,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" tele "gopkg.in/telebot.v4" + "strconv" "testing" "time" ) @@ -33,9 +35,18 @@ func TestMyGroups(t *testing.T) { gomock.InOrder( mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).Times(1), grouper.EXPECT().Groups(int64(1), "trace_id").Return(mockGroups, nil).Times(1), - ser.EXPECT().Serialize(mockGroups[0], "trace_id").Return("ser-g1", nil).Times(1), - ser.EXPECT().Serialize(mockGroups[1], "trace_id").Return("ser-g2", nil).Times(1), - ser.EXPECT().Serialize(mockGroups[2], "trace_id").Return("", errors.New("ser")).Times(1), + ser.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{strconv.Itoa(mockGroups[0].GroupID)}, + }).Return("ser-g1", nil).Times(1), + ser.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{strconv.Itoa(mockGroups[1].GroupID)}, + }).Return("ser-g2", nil).Times(1), + ser.EXPECT().Serialize(domain.SerializeMessage{ + Type: domain.GroupType, + Data: []string{strconv.Itoa(mockGroups[2].GroupID)}, + }).Return("", errors.New("ser")).Times(1), mctx.EXPECT().Send(mockStringRet, tele.ModeMarkdown, keyboards.RefreshGroups()).Return(nil).Times(1), ) From 64833f0bfeaa055ba7087fdb22ef002568affbfd Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 15:47:19 +0300 Subject: [PATCH 32/44] change sending refresh groups method --- internal/telegram/handlers/callback/refreshGroups.go | 5 +++++ test/telegram/handlers/refreshGroups_test.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/internal/telegram/handlers/callback/refreshGroups.go b/internal/telegram/handlers/callback/refreshGroups.go index 3c9a24a..bb03794 100644 --- a/internal/telegram/handlers/callback/refreshGroups.go +++ b/internal/telegram/handlers/callback/refreshGroups.go @@ -24,6 +24,11 @@ func RefreshGroup(refresher GroupRefresher, log *slog.Logger) telebot.HandlerFun ) uid := ctx.Sender().ID + if err := ctx.Edit("⚙️ Обновляю группы..."); err != nil { + log.Warn("error while editing message", sl.Err(err)) + return fmt.Errorf("%s error while editing message: %w", op, err) + } + if err := refresher.RefreshGroup(uid, traceID); err != nil { if errors.Is(err, groups.ErrNoGroups) { return ctx.Edit("У вас не нашлось ни 1 группы!\nПроверьте ваши cookie") diff --git a/test/telegram/handlers/refreshGroups_test.go b/test/telegram/handlers/refreshGroups_test.go index c03c451..d1f548b 100644 --- a/test/telegram/handlers/refreshGroups_test.go +++ b/test/telegram/handlers/refreshGroups_test.go @@ -27,6 +27,7 @@ func TestRefreshGroups(t *testing.T) { mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).AnyTimes() t.Run("happy path", func(t *testing.T) { gomock.InOrder( + mctx.EXPECT().Edit("⚙️ Обновляю группы..."), refresher.EXPECT().RefreshGroup(int64(1), "").Return(nil), mctx.EXPECT().Edit("Успешно обновлено!"), ) @@ -35,6 +36,7 @@ func TestRefreshGroups(t *testing.T) { }) t.Run("nop groups found", func(t *testing.T) { gomock.InOrder( + mctx.EXPECT().Edit("⚙️ Обновляю группы..."), refresher.EXPECT().RefreshGroup(int64(1), "").Return(groups.ErrNoGroups), mctx.EXPECT().Edit("У вас не нашлось ни 1 группы!\nПроверьте ваши cookie"), ) @@ -45,6 +47,7 @@ func TestRefreshGroups(t *testing.T) { errExp := errors.New("exp") gomock.InOrder( + mctx.EXPECT().Edit("⚙️ Обновляю группы..."), refresher.EXPECT().RefreshGroup(int64(1), "").Return(errExp), ) err := handler(mctx) From 4a004232ad6dcc0e64ad14535db099852c8688d0 Mon Sep 17 00:00:00 2001 From: pavlov Date: Thu, 10 Apr 2025 16:16:39 +0300 Subject: [PATCH 33/44] add missing kids handler + test, start writing service --- internal/domain/models/currentGroup.go | 14 ++++ internal/services/groups/currentGroup.go | 28 +++++++ internal/services/groups/group.go | 4 +- internal/services/groups/refreshGroups.go | 4 +- .../telegram/handlers/text/missingKids.go | 66 +++++++++++++++ test/mocks/telegram/handlers/mockgen.go | 2 + test/telegram/handlers/missingKids_test.go | 84 +++++++++++++++++++ 7 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 internal/domain/models/currentGroup.go create mode 100644 internal/services/groups/currentGroup.go create mode 100644 internal/telegram/handlers/text/missingKids.go create mode 100644 test/telegram/handlers/missingKids_test.go diff --git a/internal/domain/models/currentGroup.go b/internal/domain/models/currentGroup.go new file mode 100644 index 0000000..ef5d91d --- /dev/null +++ b/internal/domain/models/currentGroup.go @@ -0,0 +1,14 @@ +package models + +type CurrentGroup struct { + GroupID int + Title string + Lesson string + LessonID int + Kids []string + MissingKids []MissingKid +} +type MissingKid struct { + Fullname string + Count int +} diff --git a/internal/services/groups/currentGroup.go b/internal/services/groups/currentGroup.go new file mode 100644 index 0000000..bae06df --- /dev/null +++ b/internal/services/groups/currentGroup.go @@ -0,0 +1,28 @@ +package groups + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "log/slog" + "time" +) + +func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (models.CurrentGroup, error) { + const op = "services.groups.CurrentGroup" + log := g.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + cookie, err := g.domain.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return models.CurrentGroup{}, fmt.Errorf("%s failed to get cookies: %w", op, err) + } + if cookie == "" { + return models.CurrentGroup{}, fmt.Errorf("%s cookie is empty: %w", op, ErrNotValidCookie) + } + + panic("todo implementation") +} diff --git a/internal/services/groups/group.go b/internal/services/groups/group.go index 99aa256..450cb78 100644 --- a/internal/services/groups/group.go +++ b/internal/services/groups/group.go @@ -22,11 +22,11 @@ type Group struct { log *slog.Logger getter GroupGetter groupFetcher GroupFetcher - domainSetter DomainSetter + domain DomainSetter } func NewGroup(log *slog.Logger, getter GroupGetter, setter DomainSetter, groupFetcher GroupFetcher) *Group { - return &Group{log: log, getter: getter, domainSetter: setter, groupFetcher: groupFetcher} + return &Group{log: log, getter: getter, domain: setter, groupFetcher: groupFetcher} } func (g *Group) Groups(uid int64, traceID interface{}) ([]models.Group, error) { diff --git a/internal/services/groups/refreshGroups.go b/internal/services/groups/refreshGroups.go index 75b5a0e..ce8cb5f 100644 --- a/internal/services/groups/refreshGroups.go +++ b/internal/services/groups/refreshGroups.go @@ -24,7 +24,7 @@ func (g *Group) RefreshGroup(uid int64, traceID interface{}) error { slog.Any("trace_id", traceID), ) - cookie, err := g.domainSetter.Cookies(uid) + cookie, err := g.domain.Cookies(uid) if err != nil { log.Warn("failed to get cookies", sl.Err(err)) return fmt.Errorf("%s failed to get cookies: %w", op, err) @@ -43,7 +43,7 @@ func (g *Group) RefreshGroup(uid int64, traceID interface{}) error { return fmt.Errorf("%s no groups found: %w", op, ErrNoGroups) } - if err := g.domainSetter.SetGroups(uid, groups); err != nil { + if err := g.domain.SetGroups(uid, groups); err != nil { log.Warn("failed to set groups", sl.Err(err)) return fmt.Errorf("%s failed to set groups: %w", op, err) } diff --git a/internal/telegram/handlers/text/missingKids.go b/internal/telegram/handlers/text/missingKids.go new file mode 100644 index 0000000..b388e52 --- /dev/null +++ b/internal/telegram/handlers/text/missingKids.go @@ -0,0 +1,66 @@ +package text + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "algobot/internal/services/groups" + "errors" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" + "time" +) + +type ActualGroup interface { + CurrentGroup(uid int64, time time.Time, traceID interface{}) (models.CurrentGroup, error) +} + +func NewMissingKids(log *slog.Logger, actualGroup ActualGroup) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewMissingKids" + traceID := ctx.Get("trace_id") + uid := ctx.Sender().ID + + log := log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + group, err := actualGroup.CurrentGroup(uid, time.Now(), traceID) + if err != nil { + if errors.Is(err, groups.ErrNoGroups) { + return ctx.Send("В данный момент, никакой группы не найдено!") + } + if errors.Is(err, groups.ErrNotValidCookie) { + return ctx.Send("Вам необходимо установить свои cookie!") + } + log.Warn("error while fetching CurrentGroup", sl.Err(err)) + return fmt.Errorf("%s error while fetching CurrentGroup: %w", op, err) + } + + return ctx.Send(getMsg(group), telebot.ModeMarkdown) + } +} + +func getMsg(gr models.CurrentGroup) string { + miss := strings.Builder{} + miss.WriteString("\n```Отсутствующие\n") + for _, kid := range gr.MissingKids { + miss.WriteString(kid.Fullname) + if kid.Count > 1 { + miss.WriteString(fmt.Sprintf(" (Уже %d занятие)", kid.Count)) + } + miss.WriteString("\n") + } + miss.WriteString("```") + + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("Группа: %s", gr.Title)) + sb.WriteString(fmt.Sprintf("\nЛекция: %s\n", gr.Lesson)) + sb.WriteString(fmt.Sprintf("\nОбщее число детей: %d", len(gr.Kids))) + sb.WriteString(fmt.Sprintf("\nОтсутствуют: %d\n", len(gr.MissingKids))) + sb.WriteString(miss.String()) + + return sb.String() +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 3076b5c..304b8ef 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -23,3 +23,5 @@ package mocks //go:generate mockgen -destination=./viewFetcher_mock.go -package=mocks algobot/internal/telegram/handlers/text ViewFetcher //go:generate mockgen -destination=./serializator_mock.go -package=mocks algobot/internal/telegram/handlers/text Serializator + +//go:generate mockgen -destination=./actualGroup_mock.go -package=mocks algobot/internal/telegram/handlers/text ActualGroup diff --git a/test/telegram/handlers/missingKids_test.go b/test/telegram/handlers/missingKids_test.go new file mode 100644 index 0000000..1872796 --- /dev/null +++ b/test/telegram/handlers/missingKids_test.go @@ -0,0 +1,84 @@ +package test + +import ( + "algobot/internal/domain/models" + "algobot/internal/services/groups" + "algobot/internal/telegram/handlers/text" + mocks2 "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" +) + +func TestMissingKids(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + actualGroup := mocks.NewMockActualGroup(ctrl) + log := mocks2.NewMockLogger() + mctx := mocks3.NewMockContext(ctrl) + + handler := text.NewMissingKids(log, actualGroup) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&telebot.User{ID: 1}).AnyTimes() + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + actualGroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(acGroupasset, nil).Times(1), + mctx.EXPECT().Send("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", telebot.ModeMarkdown).Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("no groups found", func(t *testing.T) { + gomock.InOrder( + actualGroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, groups.ErrNoGroups).Times(1), + mctx.EXPECT().Send("В данный момент, никакой группы не найдено!").Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("ErrNotValidCookie", func(t *testing.T) { + gomock.InOrder( + actualGroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, groups.ErrNotValidCookie).Times(1), + mctx.EXPECT().Send("Вам необходимо установить свои cookie!").Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("CurrentGroup return err", func(t *testing.T) { + errExp := errors.New("err") + gomock.InOrder( + actualGroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, errExp).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) + +} + +var acGroupasset = models.CurrentGroup{ + GroupID: 1, + Title: "title", + Lesson: "lesson", + LessonID: 1, + Kids: []string{ + "1", + "2", + "3", + }, + MissingKids: []models.MissingKid{ + { + Fullname: "1", + Count: 2, + }, + { + Fullname: "1", + Count: 1, + }, + }, +} From 054033b4464eafe46e62122d3203c6015d514a98 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 11 Apr 2025 10:33:08 +0300 Subject: [PATCH 34/44] add missing kids service + test --- internal/app/telegram/app.go | 3 +- internal/domain/models/currentGroup.go | 1 + internal/lib/backoffice/kidsStats.go | 32 ++++ internal/services/groups/currentGroup.go | 99 +++++++++++- internal/services/groups/group.go | 59 ++++++- test/lib/backoffice/KidsStats_example | 51 +++++++ test/lib/backoffice/backoffice_test.go | 25 +++ test/mocks/services/mockgen.go | 2 + test/services/backoffice_test.go | 1 + test/services/group_test.go | 169 ++++++++++++++++++++- test/telegram/handlers/missingKids_test.go | 1 - 11 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 internal/lib/backoffice/kidsStats.go create mode 100644 test/lib/backoffice/KidsStats_example diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index 889c408..f16f99d 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -63,7 +63,7 @@ func New( } // dependencies - groupServ := groups.NewGroup(log, storage, storage, bo) + groupServ := groups.NewGroup(log, storage, bo, storage, bo) stateMachine := memory.New() serdes := base62.NewSerdes(log) grpc := grpc2.NewAIService( @@ -90,6 +90,7 @@ func New( r.HandleFuncText("Настройки", text.NewSettings(storage, log)) r.HandleFuncText("AI 🔹", text.NewAI(grpc, log, stateMachine)) r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) + r.HandleFuncText("Получить отсутсвующих", text.NewMissingKids(log, groupServ)) r.HandleRegexpText(regexp.MustCompile(`^(?m)\/start\s(.+)$`), text.NewViewInformer(serdes, boSvc, log, b.Me.Username)) diff --git a/internal/domain/models/currentGroup.go b/internal/domain/models/currentGroup.go index ef5d91d..127b900 100644 --- a/internal/domain/models/currentGroup.go +++ b/internal/domain/models/currentGroup.go @@ -10,5 +10,6 @@ type CurrentGroup struct { } type MissingKid struct { Fullname string + KidID int Count int } diff --git a/internal/lib/backoffice/kidsStats.go b/internal/lib/backoffice/kidsStats.go new file mode 100644 index 0000000..897dcf6 --- /dev/null +++ b/internal/lib/backoffice/kidsStats.go @@ -0,0 +1,32 @@ +package backoffice + +import ( + "algobot/internal/domain/backoffice" + "encoding/json" + "fmt" + "strconv" +) + +func (bo *Backoffice) KidsStats(cookie string, groupID int) (backoffice.KidsStats, error) { + const op = "backoffice.KidsStats" + + req, err := bo.createReq("GET", "/api/v1/stats/default/attendance", cookie, map[string]string{ + "group": strconv.Itoa(groupID), + }, nil) + if err != nil { + return backoffice.KidsStats{}, fmt.Errorf("%s err createReq: %w", op, err) + } + + data, reqErr := bo.doReq(req) + if reqErr != nil { + return backoffice.KidsStats{}, fmt.Errorf("%s err doReq: %w", op, err) + } + + var response backoffice.KidsStats + err = json.NewDecoder(data.Body).Decode(&response) + if err != nil { + return backoffice.KidsStats{}, fmt.Errorf("%s err while docding: %w", op, err) + } + + return response, nil +} diff --git a/internal/services/groups/currentGroup.go b/internal/services/groups/currentGroup.go index bae06df..f498529 100644 --- a/internal/services/groups/currentGroup.go +++ b/internal/services/groups/currentGroup.go @@ -1,13 +1,22 @@ package groups import ( + "algobot/internal/domain/backoffice" "algobot/internal/domain/models" "algobot/internal/lib/logger/sl" "fmt" + "log" "log/slog" + "regexp" + "strconv" "time" ) +type KidStats interface { + KidsStats(cookie string, groupID int) (backoffice.KidsStats, error) + KidsNamesByGroup(groupId string, cookie string) (backoffice.NamesByGroup, error) +} + func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (models.CurrentGroup, error) { const op = "services.groups.CurrentGroup" log := g.log.With( @@ -24,5 +33,93 @@ func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (mo return models.CurrentGroup{}, fmt.Errorf("%s cookie is empty: %w", op, ErrNotValidCookie) } - panic("todo implementation") + group, err := g.Groups(uid, traceID) + if err != nil { + log.Warn("failed to get groups", sl.Err(err)) + return models.CurrentGroup{}, fmt.Errorf("%s failed to get groups: %w", op, err) + } + + m := models.CurrentGroup{} + missingKids := make(map[int]models.MissingKid) + + actual, err := CurrentGroup(time, group) + if err != nil { + return models.CurrentGroup{}, fmt.Errorf("%s : %w", op, err) + } + m.Title = actual.Title + m.GroupID = actual.GroupID + + stats, err := g.kidsStats.KidsStats(cookie, actual.GroupID) + if err != nil { + log.Warn("error while fetching kids stats", sl.Err(err)) + return models.CurrentGroup{}, fmt.Errorf("%s error while fetching kids stats: %w", op, err) + } + for _, datum := range stats.Data { + studentID := datum.StudentID + count := 0 + for _, attendance := range datum.Attendance { + count++ + if attendance.Status != "absent" { + count = 0 + } + if matchDates(attendance.StartTimeFormatted, time) { + m.Lesson = attendance.LessonTitle + m.LessonID = attendance.LessonID + + if attendance.Status == "absent" { + missingKids[studentID] = models.MissingKid{ + Fullname: "", + Count: count, + KidID: studentID, + } + break + } + } + } + } + + names, err := g.kidsStats.KidsNamesByGroup(strconv.Itoa(actual.GroupID), cookie) + if err != nil { + log.Warn("error while fetching KidsNamesByGroup", sl.Err(err)) + return models.CurrentGroup{}, fmt.Errorf("%s error while fetching KidsNamesByGroup: %w", op, err) + } + for _, datum := range names.Data.Items { + if datum.LastGroup.ID == actual.GroupID && datum.LastGroup.Status == 0 { + missingKids[datum.ID] = models.MissingKid{ + Fullname: datum.FullName, + Count: missingKids[datum.ID].Count, + KidID: missingKids[datum.ID].KidID, + } + } + } + + split(&m, missingKids) + + return m, nil +} + +func split(m *models.CurrentGroup, kids map[int]models.MissingKid) { + m.MissingKids = make([]models.MissingKid, 0, len(kids)) + for _, kid := range kids { + if kid.Count != 0 { + m.MissingKids = append(m.MissingKids, kid) + } + m.Kids = append(m.Kids, kid.Fullname) + } +} + +func matchDates(timeStr string, t time.Time) bool { + timeStr = regexp.MustCompile(`^[а-яА-Я]+(\s+)?`).ReplaceAllString(timeStr, "") + timeFormatted, err := time.Parse("02.01.06 15:04", timeStr) + + if err != nil { + log.Printf("Cant convert date str to Time - '%s'\n", timeStr) + return false + } + + if t.YearDay() == timeFormatted.YearDay() { + return true + } + + return false } diff --git a/internal/services/groups/group.go b/internal/services/groups/group.go index 450cb78..1b9d8d8 100644 --- a/internal/services/groups/group.go +++ b/internal/services/groups/group.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log/slog" + "time" ) var ( @@ -23,10 +24,23 @@ type Group struct { getter GroupGetter groupFetcher GroupFetcher domain DomainSetter + kidsStats KidStats } -func NewGroup(log *slog.Logger, getter GroupGetter, setter DomainSetter, groupFetcher GroupFetcher) *Group { - return &Group{log: log, getter: getter, domain: setter, groupFetcher: groupFetcher} +func NewGroup( + log *slog.Logger, + getter GroupGetter, + groupFetcher GroupFetcher, + domain DomainSetter, + kidsStats KidStats, +) *Group { + return &Group{ + log: log, + getter: getter, + groupFetcher: groupFetcher, + domain: domain, + kidsStats: kidsStats, + } } func (g *Group) Groups(uid int64, traceID interface{}) ([]models.Group, error) { @@ -46,3 +60,44 @@ func (g *Group) Groups(uid int64, traceID interface{}) ([]models.Group, error) { return groups, nil } + +func CurrentGroup(t time.Time, g []models.Group) (models.Group, error) { + current, err := GroupsByDay(t, g) + if err != nil { + return models.Group{}, err + } + for _, group := range current { + if inDiapazon(-30, 90, t, group.TimeLesson) { + return group, nil + } + } + + return models.Group{}, ErrNoGroups +} +func GroupsByDay(t time.Time, g []models.Group) ([]models.Group, error) { + var filtered []models.Group + + for _, group := range g { + if t.Weekday() == group.TimeLesson.Weekday() { + filtered = append(filtered, group) + } + } + if len(filtered) == 0 { + return nil, ErrNoGroups + } + + return filtered, nil +} +func inDiapazon(start, end int, now, group time.Time) bool { + s := group.Add(time.Duration(start) * time.Minute) + e := group.Add(time.Duration(end) * time.Minute) + + startTime := time.Date(1970, 1, 1, s.Hour(), s.Minute(), 0, 0, time.UTC) + endTime := time.Date(1970, 1, 1, e.Hour(), e.Minute(), 0, 0, time.UTC) + currentTime := time.Date(1970, 1, 1, now.Hour(), now.Minute(), 0, 0, time.UTC) + + if currentTime.After(startTime) && currentTime.Before(endTime) { + return true + } + return false +} diff --git a/test/lib/backoffice/KidsStats_example b/test/lib/backoffice/KidsStats_example new file mode 100644 index 0000000..941048e --- /dev/null +++ b/test/lib/backoffice/KidsStats_example @@ -0,0 +1,51 @@ +{ + "status": "success", + "data": [ + { + "student_id": 12345678, + "attendance": [ + { + "lesson_id": 1001, + "lesson_title": "Знакомство с компьютером и цифровым миром", + "start_time_formatted": "пн 15.01.24 10:00", + "status": "inactive" + }, + { + "lesson_id": 1002, + "lesson_title": "Файлы, папки и как ими пользоваться", + "start_time_formatted": "пн 22.01.24 10:00", + "status": "inactive" + }, + { + "lesson_id": 1003, + "lesson_title": "Интернет и как искать информацию", + "start_time_formatted": "пн 29.01.24 10:00", + "status": "inactive" + } + ] + }, + { + "student_id": 87654321, + "attendance": [ + { + "lesson_id": 1001, + "lesson_title": "Знакомство с компьютером и цифровым миром", + "start_time_formatted": "пн 15.01.24 10:00", + "status": "present" + }, + { + "lesson_id": 1002, + "lesson_title": "Файлы, папки и как ими пользоваться", + "start_time_formatted": "пн 22.01.24 10:00", + "status": "present" + }, + { + "lesson_id": 1003, + "lesson_title": "Интернет и как искать информацию", + "start_time_formatted": "пн 29.01.24 10:00", + "status": "present" + } + ] + } + ] +} diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go index 0b417a1..9712e28 100644 --- a/test/lib/backoffice/backoffice_test.go +++ b/test/lib/backoffice/backoffice_test.go @@ -100,6 +100,24 @@ func TestBackoffice(t *testing.T) { assert.NoError(t, err) assert.Equal(t, backofficeKidViewExpected, group) }) + t.Run("KidsStats", func(t *testing.T) { + responseHTML := readFile("KidsStats_example") + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + group, err := bo.KidsStats("", 123) + assert.NoError(t, err) + assert.Equal(t, backofficeKidsStats, group) + }) } func readFile(fileName string) string { @@ -115,6 +133,13 @@ func readFile(fileName string) string { return responseHTML } +var backofficeKidsStats = backoffice2.KidsStats{ + Status: "success", + Data: []backoffice2.KidStat{{ + StudentID: 12345678, + Attendance: []backoffice2.Attendance{{LessonID: 1001, LessonTitle: "Знакомство с компьютером и цифровым миром", StartTimeFormatted: "пн 15.01.24 10:00", Status: "inactive"}, {LessonID: 1002, LessonTitle: "Файлы, папки и как ими пользоваться", StartTimeFormatted: "пн 22.01.24 10:00", Status: "inactive"}, {LessonID: 1003, LessonTitle: "Интернет и как искать информацию", StartTimeFormatted: "пн 29.01.24 10:00", Status: "inactive"}}, + }, {StudentID: 87654321, Attendance: []backoffice2.Attendance{{LessonID: 1001, LessonTitle: "Знакомство с компьютером и цифровым миром", StartTimeFormatted: "пн 15.01.24 10:00", Status: "present"}, {LessonID: 1002, LessonTitle: "Файлы, папки и как ими пользоваться", StartTimeFormatted: "пн 22.01.24 10:00", Status: "present"}, {LessonID: 1003, LessonTitle: "Интернет и как искать информацию", StartTimeFormatted: "пн 29.01.24 10:00", Status: "present"}}}}, +} var backofficeGroupViewExpected = backoffice2.GroupInfo{ Status: "success", Data: backoffice2.GroupDataFull{ID: 12345678, Title: "Занятие в библиотеке 5 в 14.00", Content: "Группа по курсу Python", Type: backoffice2.TypeFull{Value: "regular", Label: "Группа", Tag: "default"}, Status: backoffice2.StatusFull{Value: 10, Label: "Активная", Tag: "success"}, StatusChangedAt: "20.09.2024 12:04", StartTime: "22.09.2024 14:00", NextLessonTime: "13.04.2025 14:00", LessonsTotal: 35, LessonsPassed: 28, HardwareNeeded: 0, Branch: backoffice2.BranchFull{ diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index 57a4e2e..315120d 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -9,3 +9,5 @@ package mocks //go:generate mockgen -destination=./groupView_mock.go -package=mocks algobot/internal/services/backoffice GroupView //go:generate mockgen -destination=./kidViewer_mock.go -package=mocks algobot/internal/services/backoffice KidViewer //go:generate mockgen -destination=./cookieGetter_mock.go -package=mocks algobot/internal/services/backoffice CookieGetter + +//go:generate mockgen -destination=./kidStats_mock.go -package=mocks algobot/internal/services/groups KidStats diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 509bbd7..125f96d 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -133,6 +133,7 @@ func TestBackoffice(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) + } var KidViewBackoffice = backoffice2.KidView{ diff --git a/test/services/group_test.go b/test/services/group_test.go index b22651d..e36fcee 100644 --- a/test/services/group_test.go +++ b/test/services/group_test.go @@ -1,6 +1,7 @@ package test import ( + "algobot/internal/domain/backoffice" "algobot/internal/domain/models" "algobot/internal/services/groups" "algobot/test/mocks" @@ -20,12 +21,14 @@ func TestGroup(t *testing.T) { gGetter := mocks2.NewMockGroupGetter(ctrl) setter := mocks2.NewMockDomainSetter(ctrl) fetcher := mocks2.NewMockGroupFetcher(ctrl) + kidStats := mocks2.NewMockKidStats(ctrl) service := groups.NewGroup( log, gGetter, - setter, fetcher, + setter, + kidStats, ) t.Run("Groups", func(t *testing.T) { @@ -127,6 +130,93 @@ func TestGroup(t *testing.T) { assert.ErrorIs(t, err, errExp2) }) }) + t.Run("CurrentGroup", func(t *testing.T) { + userID := int64(1) + timeStub := time.Date(2025, time.March, 23, 14, 0, 0, 0, time.UTC) + traceID := "" + cookie := "cookie" + errExp := errors.New("") + + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(userID).Return(cookie, nil).Times(1), + gGetter.EXPECT().Groups(userID).Return(assets, nil).Times(1), + kidStats.EXPECT().KidsStats(cookie, 1001).Return(kidsStats, nil).Times(1), + kidStats.EXPECT().KidsNamesByGroup("1001", cookie).Return(KidsNames, nil).Times(1), + ) + + group, err := service.CurrentGroup(userID, timeStub, traceID) + assert.NoError(t, err) + assert.Equal( + t, + models.CurrentGroup{ + GroupID: 1001, + Title: "group 1", + Lesson: "LessonTitle1", + LessonID: 1, + Kids: []string{"FullName1", "FullName0"}, + MissingKids: []models.MissingKid{{ + "FullName1", + 1, + 2, + }}, + }, + group, + ) + }) + t.Run("Cookies return err", func(t *testing.T) { + t.Run("Empty cookie", func(t *testing.T) { + setter.EXPECT().Cookies(userID).Return("", nil).Times(1) + _, err := service.CurrentGroup(userID, timeStub, traceID) + assert.ErrorIs(t, err, groups.ErrNotValidCookie) + }) + t.Run("Cookies return error", func(t *testing.T) { + setter.EXPECT().Cookies(userID).Return("", errExp).Times(1) + _, err := service.CurrentGroup(userID, timeStub, traceID) + assert.ErrorIs(t, err, errExp) + }) + }) + t.Run("Groups return err", func(t *testing.T) { + + gomock.InOrder( + setter.EXPECT().Cookies(userID).Return(cookie, nil).Times(1), + gGetter.EXPECT().Groups(userID).Return(nil, errExp).Times(1), + ) + + _, err := service.CurrentGroup(userID, timeStub, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("returns ErrNoGroups ", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(userID).Return(cookie, nil).Times(1), + gGetter.EXPECT().Groups(userID).Return(assets, nil).Times(1), + ) + + _, err := service.CurrentGroup(userID, time.Date(2025, time.April, 2025, 22, 0, 0, 0, time.UTC), traceID) + assert.ErrorIs(t, err, groups.ErrNoGroups) + }) + t.Run("KidsStats return err", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(userID).Return(cookie, nil).Times(1), + gGetter.EXPECT().Groups(userID).Return(assets, nil).Times(1), + kidStats.EXPECT().KidsStats(cookie, 1001).Return(backoffice.KidsStats{}, errExp).Times(1), + ) + + _, err := service.CurrentGroup(userID, timeStub, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("KidsNamesByGroup return err", func(t *testing.T) { + gomock.InOrder( + setter.EXPECT().Cookies(userID).Return(cookie, nil).Times(1), + gGetter.EXPECT().Groups(userID).Return(assets, nil).Times(1), + kidStats.EXPECT().KidsStats(cookie, 1001).Return(kidsStats, nil).Times(1), + kidStats.EXPECT().KidsNamesByGroup("1001", cookie).Return(backoffice.NamesByGroup{}, errExp).Times(1), + ) + + _, err := service.CurrentGroup(userID, timeStub, traceID) + assert.ErrorIs(t, err, errExp) + }) + }) } var assets = []models.Group{ @@ -147,3 +237,80 @@ var assets = []models.Group{ TimeLesson: time.Date(2025, time.March, 22, 14, 0, 0, 0, time.UTC), }, } +var KidsNames = backoffice.NamesByGroup{ + Status: "", + Data: backoffice.GroupData{ + Items: []backoffice.Student{ + { + ID: 0, + FullName: "FullName0", + LastGroup: backoffice.Group{ + Status: 0, + ID: 1001, + }, + }, + { + ID: 1, + FullName: "FullName1", + LastGroup: backoffice.Group{ + Status: 0, + ID: 1001, + }, + }, + }, + }, +} + +var ( + kidsStats = backoffice.KidsStats{ + Status: "", + Data: []backoffice.KidStat{ + { + StudentID: 0, + Attendance: []backoffice.Attendance{ + { + LessonID: 0, + LessonTitle: "LessonTitle0", + StartTimeFormatted: "вс 12.03.25 14:00", + Status: "present", + }, + { + LessonID: 1, + LessonTitle: "LessonTitle1", + StartTimeFormatted: "вс 23.03.25 14:00", + Status: "present", + }, + { + LessonID: 2, + LessonTitle: "LessonTitle2", + StartTimeFormatted: "вс 23.04.25 14:00", + Status: "future", + }, + }, + }, + { + StudentID: 1, + Attendance: []backoffice.Attendance{ + { + LessonID: 0, + LessonTitle: "LessonTitle0", + StartTimeFormatted: "вс 12.03.25 14:00", + Status: "absent", + }, + { + LessonID: 1, + LessonTitle: "LessonTitle1", + StartTimeFormatted: "вс 23.03.25 14:00", + Status: "absent", + }, + { + LessonID: 2, + LessonTitle: "LessonTitle2", + StartTimeFormatted: "вс 23.04.25 14:00", + Status: "future", + }, + }, + }, + }, + } +) diff --git a/test/telegram/handlers/missingKids_test.go b/test/telegram/handlers/missingKids_test.go index 1872796..6f756ae 100644 --- a/test/telegram/handlers/missingKids_test.go +++ b/test/telegram/handlers/missingKids_test.go @@ -58,7 +58,6 @@ func TestMissingKids(t *testing.T) { err := handler(mctx) assert.ErrorIs(t, err, errExp) }) - } var acGroupasset = models.CurrentGroup{ From 8357c4081da17b949671a07f9f3e35df21a58207 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 11 Apr 2025 11:39:09 +0300 Subject: [PATCH 35/44] add /abs command --- internal/app/telegram/app.go | 2 +- internal/services/groups/currentGroup.go | 6 +- internal/telegram/handlers/text/absentKids.go | 49 ++++++++++ .../telegram/handlers/text/missingKids.go | 4 +- test/telegram/handlers/absentKids_test.go | 96 +++++++++++++++++++ test/telegram/handlers/missingKids_test.go | 2 + 6 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 internal/telegram/handlers/text/absentKids.go create mode 100644 test/telegram/handlers/absentKids_test.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index f16f99d..c54c8ff 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -91,6 +91,7 @@ func New( r.HandleFuncText("AI 🔹", text.NewAI(grpc, log, stateMachine)) r.HandleText("Мои группы", text.NewMyGroup(log, groupServ, serdes, b.Me.Username)) r.HandleFuncText("Получить отсутсвующих", text.NewMissingKids(log, groupServ)) + r.HandleFuncRegexpText(regexp.MustCompile(`^(?m)\/abs(.*)$`), text.NewAbsentKids(groupServ, log)) r.HandleRegexpText(regexp.MustCompile(`^(?m)\/start\s(.+)$`), text.NewViewInformer(serdes, boSvc, log, b.Me.Username)) @@ -116,7 +117,6 @@ func New( r.HandleFuncText("/reset", text.NewReset(grpc, log)) r.HandleFuncRegexpText(regexp.MustCompile(`^(?m)\/image\s(.+)$`), text.GenerateImage(grpc, log)) r.HandleFuncRegexpText(regexp.MustCompile(`^[^/].*$`), text.ChatAI(grpc, log)) - }) r.NotFound(text.NewStart(stateMachine)) diff --git a/internal/services/groups/currentGroup.go b/internal/services/groups/currentGroup.go index f498529..f1d2e45 100644 --- a/internal/services/groups/currentGroup.go +++ b/internal/services/groups/currentGroup.go @@ -9,6 +9,7 @@ import ( "log/slog" "regexp" "strconv" + "strings" "time" ) @@ -54,6 +55,7 @@ func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (mo log.Warn("error while fetching kids stats", sl.Err(err)) return models.CurrentGroup{}, fmt.Errorf("%s error while fetching kids stats: %w", op, err) } + for _, datum := range stats.Data { studentID := datum.StudentID count := 0 @@ -83,8 +85,10 @@ func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (mo log.Warn("error while fetching KidsNamesByGroup", sl.Err(err)) return models.CurrentGroup{}, fmt.Errorf("%s error while fetching KidsNamesByGroup: %w", op, err) } + for _, datum := range names.Data.Items { if datum.LastGroup.ID == actual.GroupID && datum.LastGroup.Status == 0 { + missingKids[datum.ID] = models.MissingKid{ Fullname: datum.FullName, Count: missingKids[datum.ID].Count, @@ -101,7 +105,7 @@ func (g *Group) CurrentGroup(uid int64, time time.Time, traceID interface{}) (mo func split(m *models.CurrentGroup, kids map[int]models.MissingKid) { m.MissingKids = make([]models.MissingKid, 0, len(kids)) for _, kid := range kids { - if kid.Count != 0 { + if kid.Count != 0 && strings.TrimSpace(kid.Fullname) != "" { m.MissingKids = append(m.MissingKids, kid) } m.Kids = append(m.Kids, kid.Fullname) diff --git a/internal/telegram/handlers/text/absentKids.go b/internal/telegram/handlers/text/absentKids.go new file mode 100644 index 0000000..d75c9dd --- /dev/null +++ b/internal/telegram/handlers/text/absentKids.go @@ -0,0 +1,49 @@ +package text + +import ( + "algobot/internal/lib/logger/sl" + "algobot/internal/services/groups" + "errors" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" + "time" +) + +func NewAbsentKids(actualGroup ActualGroup, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "text.NewAbsentKids" + + uid := ctx.Sender().ID + traceID := ctx.Get("trace_id") + data := getDate(ctx.Message().Text) + + log := log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + date, err := time.Parse("2006-01-02 15:04", data) + if err != nil { + return ctx.Reply("Не удалось распарсить дату, пожалуйста, введите дату в формате YYYY-MM-DD HH:MM") + } + + group, err := actualGroup.CurrentGroup(uid, date, traceID) + if err != nil { + if errors.Is(err, groups.ErrNoGroups) { + return ctx.Send("В данный момент, никакой группы не найдено!") + } + if errors.Is(err, groups.ErrNotValidCookie) { + return ctx.Send("Вам необходимо установить свои cookie!") + } + log.Warn("error while fetching CurrentGroup", sl.Err(err)) + return fmt.Errorf("%s error while fetching CurrentGroup: %w", op, err) + } + + return ctx.Reply(GetMissingMessage(group), telebot.ModeMarkdown) + } +} +func getDate(text string) string { + return strings.TrimSpace(strings.TrimLeft(text, "/abs")) +} diff --git a/internal/telegram/handlers/text/missingKids.go b/internal/telegram/handlers/text/missingKids.go index b388e52..ff41fd0 100644 --- a/internal/telegram/handlers/text/missingKids.go +++ b/internal/telegram/handlers/text/missingKids.go @@ -39,11 +39,11 @@ func NewMissingKids(log *slog.Logger, actualGroup ActualGroup) telebot.HandlerFu return fmt.Errorf("%s error while fetching CurrentGroup: %w", op, err) } - return ctx.Send(getMsg(group), telebot.ModeMarkdown) + return ctx.Send(GetMissingMessage(group), telebot.ModeMarkdown) } } -func getMsg(gr models.CurrentGroup) string { +func GetMissingMessage(gr models.CurrentGroup) string { miss := strings.Builder{} miss.WriteString("\n```Отсутствующие\n") for _, kid := range gr.MissingKids { diff --git a/test/telegram/handlers/absentKids_test.go b/test/telegram/handlers/absentKids_test.go new file mode 100644 index 0000000..398c4c6 --- /dev/null +++ b/test/telegram/handlers/absentKids_test.go @@ -0,0 +1,96 @@ +package test + +import ( + "algobot/internal/domain/models" + "algobot/internal/services/groups" + "algobot/internal/telegram/handlers/text" + mocks3 "algobot/test/mocks" + mocks2 "algobot/test/mocks/telegram" + mocks "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestNewAbsentKids(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mctx := mocks2.NewMockContext(ctrl) + log := mocks3.NewMockLogger() + agroup := mocks.NewMockActualGroup(ctrl) + + handler := text.NewAbsentKids(agroup, log) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).AnyTimes() + t.Run("Happy path", func(t *testing.T) { + mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 2025-04-06 14:44"}).Times(1) + agroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(grAsset, nil).Times(1) + mctx.EXPECT().Reply("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", tele.ModeMarkdown).Times(1) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("Wrong date", func(t *testing.T) { + mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 212344"}).Times(1) + mctx.EXPECT().Reply("Не удалось распарсить дату, пожалуйста, введите дату в формате YYYY-MM-DD HH:MM").Times(1) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("no groups found", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 2025-04-06 14:44"}).Times(1), + + agroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, groups.ErrNoGroups).Times(1), + mctx.EXPECT().Send("В данный момент, никакой группы не найдено!").Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("ErrNotValidCookie", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 2025-04-06 14:44"}).Times(1), + agroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, groups.ErrNotValidCookie).Times(1), + mctx.EXPECT().Send("Вам необходимо установить свои cookie!").Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("CurrentGroup return err", func(t *testing.T) { + errExp := errors.New("err") + gomock.InOrder( + mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 2025-04-06 14:44"}).Times(1), + agroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(models.CurrentGroup{}, errExp).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} + +var grAsset = models.CurrentGroup{ + GroupID: 1, + Title: "title", + Lesson: "lesson", + LessonID: 1, + Kids: []string{ + "1", + "2", + "3", + }, + MissingKids: []models.MissingKid{ + { + Fullname: "1", + KidID: 1, + Count: 2, + }, + { + Fullname: "1", + KidID: 2, + Count: 1, + }, + }, +} diff --git a/test/telegram/handlers/missingKids_test.go b/test/telegram/handlers/missingKids_test.go index 6f756ae..79c1b9e 100644 --- a/test/telegram/handlers/missingKids_test.go +++ b/test/telegram/handlers/missingKids_test.go @@ -73,10 +73,12 @@ var acGroupasset = models.CurrentGroup{ MissingKids: []models.MissingKid{ { Fullname: "1", + KidID: 1, Count: 2, }, { Fullname: "1", + KidID: 2, Count: 1, }, }, From 8ff418788114a14f41fd01ccaaf307931f72d063 Mon Sep 17 00:00:00 2001 From: pavlov Date: Fri, 11 Apr 2025 12:47:23 +0300 Subject: [PATCH 36/44] add status changer --- internal/app/telegram/app.go | 5 +- .../domain/telegram/keyboards/missingKids.go | 19 +++++ internal/lib/backoffice/lession-status.go | 54 ++++++++++++++ internal/services/backoffice/backoffice.go | 13 ++-- internal/services/backoffice/lesson-status.go | 48 ++++++++++++ .../handlers/callback/changeNotification.go | 2 +- .../handlers/callback/lessonStatus.go | 46 ++++++++++++ .../handlers/callback/refreshGroups.go | 2 +- internal/telegram/handlers/text/absentKids.go | 3 +- .../telegram/handlers/text/missingKids.go | 3 +- test/lib/backoffice/backoffice_test.go | 32 ++++++++ test/mocks/services/mockgen.go | 1 + test/mocks/telegram/handlers/mockgen.go | 2 + test/services/backoffice_test.go | 62 +++++++++++++++- test/telegram/handlers/absentKids_test.go | 3 +- test/telegram/handlers/lessonStatus_test.go | 74 +++++++++++++++++++ test/telegram/handlers/missingKids_test.go | 3 +- 17 files changed, 358 insertions(+), 14 deletions(-) create mode 100644 internal/domain/telegram/keyboards/missingKids.go create mode 100644 internal/lib/backoffice/lession-status.go create mode 100644 internal/services/backoffice/lesson-status.go create mode 100644 internal/telegram/handlers/callback/lessonStatus.go create mode 100644 test/telegram/handlers/lessonStatus_test.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index c54c8ff..c5e5f3e 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -70,7 +70,7 @@ func New( cfg.GRPC, grpc2.WithLogger(log), ) - boSvc := backoffice.NewBackoffice(log, storage, bo, bo) + boSvc := backoffice.NewBackoffice(log, storage, bo, bo, bo) // initialize routes b.Use(trace.New(log)) @@ -99,6 +99,9 @@ func New( r.HandleFuncCallback("\fset_cookie", callback.NewChangeCookie(stateMachine)) r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(storage, log)) r.HandleFuncCallback("\frefresh_groups", callback.RefreshGroup(groupServ, log)) + + r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fclose_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.CloseLesson, log)) + r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fopen_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.OpenLesson, log)) }) r.Group(func(r router.Router) { // Routes for SendingCookie state diff --git a/internal/domain/telegram/keyboards/missingKids.go b/internal/domain/telegram/keyboards/missingKids.go new file mode 100644 index 0000000..a1f811c --- /dev/null +++ b/internal/domain/telegram/keyboards/missingKids.go @@ -0,0 +1,19 @@ +package keyboards + +import ( + "fmt" + "gopkg.in/telebot.v4" +) + +func MissingKids(groupID, lessonID int) *telebot.ReplyMarkup { + markup := telebot.ReplyMarkup{ResizeKeyboard: true} + markup.Inline( + markup.Row( + markup.Data("Закрыть лекцию", fmt.Sprintf("close_lesson_%d_%d", groupID, lessonID)), + markup.Data("Открыть лекцию", fmt.Sprintf("open_lesson_%d_%d", groupID, lessonID)), + ), + markup.Row(markup.Data("Получить аккаунты", fmt.Sprintf("get_creds_%d", groupID))), + ) + + return &markup +} diff --git a/internal/lib/backoffice/lession-status.go b/internal/lib/backoffice/lession-status.go new file mode 100644 index 0000000..8bdf4d7 --- /dev/null +++ b/internal/lib/backoffice/lession-status.go @@ -0,0 +1,54 @@ +package backoffice + +import ( + "fmt" + "net/url" + "strings" +) + +func (bo *Backoffice) OpenLesson(cookie, group, lession string) error { + const op = "backoffice.OpenLesson" + + params := url.Values{} + params.Add("ajaxUrl", "/api/v2/group/lesson/status") + params.Add("btnClass", "btn btn-xs btn-danger") + params.Add("status", "10") + params.Add("lessonId", lession) + params.Add("groupId", group) + query := params.Encode() + + req, err := bo.createReq("POST", "/api/v2/group/lesson/status", cookie, map[string]string{}, strings.NewReader(query)) + if err != nil { + return fmt.Errorf("%s err createReq: %w", op, err) + } + _, reqErr := bo.doReq(req) + if reqErr != nil { + return fmt.Errorf("%s err doReq: %w", op, err) + } + + return nil +} + +func (bo *Backoffice) CloseLesson(cookie, group, lession string) error { + const op = "backoffice.CloseLesson" + + params := url.Values{} + params.Add("ajaxUrl", "/api/v2/group/lesson/status") + params.Add("btnClass", "btn btn-xs btn-danger") + params.Add("status", "0") + params.Add("lessonId", lession) + params.Add("groupId", group) + query := params.Encode() + + req, err := bo.createReq("POST", "/api/v2/group/lesson/status", cookie, map[string]string{}, strings.NewReader(query)) + if err != nil { + return fmt.Errorf("%s err createReq: %w", op, err) + } + + _, reqErr := bo.doReq(req) + if reqErr != nil { + return fmt.Errorf("%s err doReq: %w", op, err) + } + + return nil +} diff --git a/internal/services/backoffice/backoffice.go b/internal/services/backoffice/backoffice.go index f8df3c2..1a9c26c 100644 --- a/internal/services/backoffice/backoffice.go +++ b/internal/services/backoffice/backoffice.go @@ -7,12 +7,13 @@ type CookieGetter interface { } type Backoffice struct { - log *slog.Logger - cookieGetter CookieGetter - groupView GroupView - kidViewer KidViewer + log *slog.Logger + cookieGetter CookieGetter + groupView GroupView + kidViewer KidViewer + lessonStatuser LessonStatuser } -func NewBackoffice(log *slog.Logger, cookieGetter CookieGetter, groupView GroupView, kidViewer KidViewer) *Backoffice { - return &Backoffice{log: log, cookieGetter: cookieGetter, groupView: groupView, kidViewer: kidViewer} +func NewBackoffice(log *slog.Logger, cookieGetter CookieGetter, groupView GroupView, kidViewer KidViewer, lessonStatus LessonStatuser) *Backoffice { + return &Backoffice{log: log, cookieGetter: cookieGetter, groupView: groupView, kidViewer: kidViewer, lessonStatuser: lessonStatus} } diff --git a/internal/services/backoffice/lesson-status.go b/internal/services/backoffice/lesson-status.go new file mode 100644 index 0000000..2df10e2 --- /dev/null +++ b/internal/services/backoffice/lesson-status.go @@ -0,0 +1,48 @@ +package backoffice + +import ( + "algobot/internal/lib/logger/sl" + "fmt" + "log/slog" +) + +type LessonStatus int + +const ( + CloseLesson LessonStatus = iota + OpenLesson +) + +type LessonStatuser interface { + OpenLesson(cookie, group, lession string) error + CloseLesson(cookie, group, lession string) error +} + +func (bo *Backoffice) SetLessonStatus(uid int64, groupID string, lessonID string, status LessonStatus, traceID interface{}) error { + const op = "services.backoffice.GetGroupView" + log := bo.log.With( + slog.String("op", op), + slog.Any("traceID", traceID), + ) + + cookie, err := bo.cookieGetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return fmt.Errorf("%s failed to get cookies: %w", op, err) + } + + switch status { + case CloseLesson: + if err := bo.lessonStatuser.CloseLesson(cookie, groupID, lessonID); err != nil { + return fmt.Errorf("%s error while CloseLesson : %w", op, err) + } + case OpenLesson: + if err := bo.lessonStatuser.OpenLesson(cookie, groupID, lessonID); err != nil { + return fmt.Errorf("%s error while OpenLesson : %w", op, err) + } + default: + return fmt.Errorf("%s invalid lesson status: %d", op, status) + } + + return nil +} diff --git a/internal/telegram/handlers/callback/changeNotification.go b/internal/telegram/handlers/callback/changeNotification.go index c0f7954..58310c9 100644 --- a/internal/telegram/handlers/callback/changeNotification.go +++ b/internal/telegram/handlers/callback/changeNotification.go @@ -14,7 +14,7 @@ type NotificationChanger interface { func NewChangeNotification(n NotificationChanger, log *slog.Logger) telebot.HandlerFunc { return func(ctx telebot.Context) error { - const op = "text.NewChangeNotification" + const op = "callback.NewChangeNotification" log := log.With( slog.String("op", op), slog.Any("trace_id", ctx.Get("trace_id")), diff --git a/internal/telegram/handlers/callback/lessonStatus.go b/internal/telegram/handlers/callback/lessonStatus.go new file mode 100644 index 0000000..2a0c335 --- /dev/null +++ b/internal/telegram/handlers/callback/lessonStatus.go @@ -0,0 +1,46 @@ +package callback + +import ( + "algobot/internal/lib/logger/sl" + "algobot/internal/services/backoffice" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type LessonStatuser interface { + SetLessonStatus(uid int64, groupID string, lessonID string, status backoffice.LessonStatus, traceID interface{}) error +} + +func LessonStatus(ls LessonStatuser, status backoffice.LessonStatus, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "callback.LessonStatus" + + traceID := ctx.Get("trace_id") + log := log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + uid := ctx.Sender().ID + + var data []string + switch status { + case backoffice.CloseLesson: + data = strings.Split(strings.TrimPrefix(ctx.Callback().Data, "\fclose_lesson_"), "_") + case backoffice.OpenLesson: + data = strings.Split(strings.TrimPrefix(ctx.Callback().Data, "\fopen_lesson_"), "_") + } + if len(data) != 2 { + log.Warn("data is not correct", slog.Any("data", data)) + return ctx.Send("⚠️ Ошибка при анализе данных от кнопки") + } + + if err := ls.SetLessonStatus(uid, data[0], data[1], status, traceID); err != nil { + log.Warn("error while refreshing group", sl.Err(err)) + return fmt.Errorf("%s error while refreshing group: %w", op, err) + } + + return ctx.Send("Статус переключен!") + } +} diff --git a/internal/telegram/handlers/callback/refreshGroups.go b/internal/telegram/handlers/callback/refreshGroups.go index bb03794..797235e 100644 --- a/internal/telegram/handlers/callback/refreshGroups.go +++ b/internal/telegram/handlers/callback/refreshGroups.go @@ -15,7 +15,7 @@ type GroupRefresher interface { func RefreshGroup(refresher GroupRefresher, log *slog.Logger) telebot.HandlerFunc { return func(ctx telebot.Context) error { - const op = "text.NewChangeNotification" + const op = "callback.NewChangeNotification" traceID := ctx.Get("trace_id") log := log.With( diff --git a/internal/telegram/handlers/text/absentKids.go b/internal/telegram/handlers/text/absentKids.go index d75c9dd..ec7ceb2 100644 --- a/internal/telegram/handlers/text/absentKids.go +++ b/internal/telegram/handlers/text/absentKids.go @@ -1,6 +1,7 @@ package text import ( + "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/logger/sl" "algobot/internal/services/groups" "errors" @@ -41,7 +42,7 @@ func NewAbsentKids(actualGroup ActualGroup, log *slog.Logger) telebot.HandlerFun return fmt.Errorf("%s error while fetching CurrentGroup: %w", op, err) } - return ctx.Reply(GetMissingMessage(group), telebot.ModeMarkdown) + return ctx.Reply(GetMissingMessage(group), keyboards.MissingKids(group.GroupID, group.LessonID), telebot.ModeMarkdown) } } func getDate(text string) string { diff --git a/internal/telegram/handlers/text/missingKids.go b/internal/telegram/handlers/text/missingKids.go index ff41fd0..094c905 100644 --- a/internal/telegram/handlers/text/missingKids.go +++ b/internal/telegram/handlers/text/missingKids.go @@ -2,6 +2,7 @@ package text import ( "algobot/internal/domain/models" + "algobot/internal/domain/telegram/keyboards" "algobot/internal/lib/logger/sl" "algobot/internal/services/groups" "errors" @@ -39,7 +40,7 @@ func NewMissingKids(log *slog.Logger, actualGroup ActualGroup) telebot.HandlerFu return fmt.Errorf("%s error while fetching CurrentGroup: %w", op, err) } - return ctx.Send(GetMissingMessage(group), telebot.ModeMarkdown) + return ctx.Send(GetMissingMessage(group), keyboards.MissingKids(group.GroupID, group.LessonID), telebot.ModeMarkdown) } } diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go index 9712e28..b0eeb24 100644 --- a/test/lib/backoffice/backoffice_test.go +++ b/test/lib/backoffice/backoffice_test.go @@ -118,6 +118,38 @@ func TestBackoffice(t *testing.T) { assert.NoError(t, err) assert.Equal(t, backofficeKidsStats, group) }) + t.Run("Lesson", func(t *testing.T) { + t.Run("OpenLesson", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + err := bo.OpenLesson("", "", "") + assert.NoError(t, err) + }) + t.Run("CloseLesson", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + err := bo.CloseLesson("", "", "") + assert.NoError(t, err) + }) + }) } func readFile(fileName string) string { diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index 315120d..e72fae5 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -9,5 +9,6 @@ package mocks //go:generate mockgen -destination=./groupView_mock.go -package=mocks algobot/internal/services/backoffice GroupView //go:generate mockgen -destination=./kidViewer_mock.go -package=mocks algobot/internal/services/backoffice KidViewer //go:generate mockgen -destination=./cookieGetter_mock.go -package=mocks algobot/internal/services/backoffice CookieGetter +//go:generate mockgen -destination=./lessonStatuser_mock.go -package=mocks algobot/internal/services/backoffice LessonStatuser //go:generate mockgen -destination=./kidStats_mock.go -package=mocks algobot/internal/services/groups KidStats diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 304b8ef..8da607a 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -25,3 +25,5 @@ package mocks //go:generate mockgen -destination=./serializator_mock.go -package=mocks algobot/internal/telegram/handlers/text Serializator //go:generate mockgen -destination=./actualGroup_mock.go -package=mocks algobot/internal/telegram/handlers/text ActualGroup + +//go:generate mockgen -destination=./lessonStatuser_mock.go -package=mocks algobot/internal/telegram/handlers/callback LessonStatuser diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 125f96d..7eca6cc 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -10,6 +10,7 @@ import ( "errors" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "strconv" "testing" "time" ) @@ -22,8 +23,9 @@ func TestBackoffice(t *testing.T) { cookieGetter := mocks2.NewMockCookieGetter(ctrl) groupView := mocks2.NewMockGroupView(ctrl) kidViewer := mocks2.NewMockKidViewer(ctrl) + lessonStatuser := mocks2.NewMockLessonStatuser(ctrl) - sbo := backoffice.NewBackoffice(log, cookieGetter, groupView, kidViewer) + sbo := backoffice.NewBackoffice(log, cookieGetter, groupView, kidViewer, lessonStatuser) t.Run("KidView", func(t *testing.T) { uid := int64(1) kidID := "1" @@ -133,7 +135,65 @@ func TestBackoffice(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) + t.Run("SetLessonStatus", func(t *testing.T) { + uid := int64(1) + groupID := 1 + lessonID := 1 + traceID := "" + cookie := "" + + t.Run("happy path close ", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + lessonStatuser.EXPECT().CloseLesson(cookie, strconv.Itoa(groupID), strconv.Itoa(lessonID)).Return(nil).Times(1), + ) + + err := sbo.SetLessonStatus(uid, strconv.Itoa(groupID), strconv.Itoa(lessonID), backoffice.CloseLesson, traceID) + assert.NoError(t, err) + }) + t.Run("happy path open", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + lessonStatuser.EXPECT().OpenLesson(cookie, strconv.Itoa(groupID), strconv.Itoa(lessonID)).Return(nil).Times(1), + ) + + err := sbo.SetLessonStatus(uid, strconv.Itoa(groupID), strconv.Itoa(lessonID), backoffice.OpenLesson, traceID) + assert.NoError(t, err) + }) + t.Run("err cookie", func(t *testing.T) { + errExp := errors.New("") + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return("", errExp).Times(1), + ) + err := sbo.SetLessonStatus(uid, strconv.Itoa(groupID), strconv.Itoa(lessonID), backoffice.OpenLesson, traceID) + assert.ErrorIs(t, err, errExp) + }) + t.Run("Err CloseLesson", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + lessonStatuser.EXPECT().CloseLesson(cookie, strconv.Itoa(groupID), strconv.Itoa(lessonID)).Return(errExp).Times(1), + ) + + err := sbo.SetLessonStatus(uid, strconv.Itoa(groupID), strconv.Itoa(lessonID), backoffice.CloseLesson, traceID) + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + t.Run("Err OpenLesson", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + cookieGetter.EXPECT().Cookies(uid).Return(cookie, nil).Times(1), + lessonStatuser.EXPECT().OpenLesson(cookie, strconv.Itoa(groupID), strconv.Itoa(lessonID)).Return(errExp).Times(1), + ) + + err := sbo.SetLessonStatus(uid, strconv.Itoa(groupID), strconv.Itoa(lessonID), backoffice.OpenLesson, traceID) + assert.Error(t, err) + assert.ErrorIs(t, err, errExp) + }) + }) } var KidViewBackoffice = backoffice2.KidView{ diff --git a/test/telegram/handlers/absentKids_test.go b/test/telegram/handlers/absentKids_test.go index 398c4c6..7923170 100644 --- a/test/telegram/handlers/absentKids_test.go +++ b/test/telegram/handlers/absentKids_test.go @@ -2,6 +2,7 @@ package test import ( "algobot/internal/domain/models" + "algobot/internal/domain/telegram/keyboards" "algobot/internal/services/groups" "algobot/internal/telegram/handlers/text" mocks3 "algobot/test/mocks" @@ -29,7 +30,7 @@ func TestNewAbsentKids(t *testing.T) { t.Run("Happy path", func(t *testing.T) { mctx.EXPECT().Message().Return(&tele.Message{Text: "/abs 2025-04-06 14:44"}).Times(1) agroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(grAsset, nil).Times(1) - mctx.EXPECT().Reply("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", tele.ModeMarkdown).Times(1) + mctx.EXPECT().Reply("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", keyboards.MissingKids(1, 1), tele.ModeMarkdown).Times(1) err := handler(mctx) assert.NoError(t, err) diff --git a/test/telegram/handlers/lessonStatus_test.go b/test/telegram/handlers/lessonStatus_test.go new file mode 100644 index 0000000..94fe01b --- /dev/null +++ b/test/telegram/handlers/lessonStatus_test.go @@ -0,0 +1,74 @@ +package test + +import ( + "algobot/internal/services/backoffice" + "algobot/internal/telegram/handlers/callback" + "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + tele "gopkg.in/telebot.v4" + "testing" +) + +func TestLessonStatus(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + ls := mocks2.NewMockLessonStatuser(ctrl) + mctx := mocks3.NewMockContext(ctrl) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&tele.User{ID: 1}).AnyTimes() + + t.Run("Happy path open", func(t *testing.T) { + handler := callback.LessonStatus(ls, backoffice.OpenLesson, log) + + gomock.InOrder( + mctx.EXPECT().Callback().Return(&tele.Callback{Data: "\fopen_lesson_1_1"}), + ls.EXPECT().SetLessonStatus(int64(1), "1", "1", backoffice.OpenLesson, "").Return(nil).Times(1), + mctx.EXPECT().Send("Статус переключен!").Return(nil).Times(1), + ) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("Happy path close", func(t *testing.T) { + handler := callback.LessonStatus(ls, backoffice.CloseLesson, log) + + gomock.InOrder( + mctx.EXPECT().Callback().Return(&tele.Callback{Data: "\fclose_lesson_1_1"}), + ls.EXPECT().SetLessonStatus(int64(1), "1", "1", backoffice.CloseLesson, "").Return(nil).Times(1), + mctx.EXPECT().Send("Статус переключен!").Return(nil).Times(1), + ) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("wrong data", func(t *testing.T) { + handler := callback.LessonStatus(ls, backoffice.CloseLesson, log) + + gomock.InOrder( + mctx.EXPECT().Callback().Return(&tele.Callback{Data: "close_lesson_1"}), + mctx.EXPECT().Send("⚠️ Ошибка при анализе данных от кнопки").Return(nil).Times(1), + ) + + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("SetLessonStatus return err", func(t *testing.T) { + errExp := errors.New("1") + handler := callback.LessonStatus(ls, backoffice.CloseLesson, log) + + gomock.InOrder( + mctx.EXPECT().Callback().Return(&tele.Callback{Data: "close_lesson_1_1"}), + ls.EXPECT().SetLessonStatus(int64(1), "1", "1", backoffice.CloseLesson, "").Return(errExp).Times(1), + ) + + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} diff --git a/test/telegram/handlers/missingKids_test.go b/test/telegram/handlers/missingKids_test.go index 79c1b9e..8d803ff 100644 --- a/test/telegram/handlers/missingKids_test.go +++ b/test/telegram/handlers/missingKids_test.go @@ -2,6 +2,7 @@ package test import ( "algobot/internal/domain/models" + "algobot/internal/domain/telegram/keyboards" "algobot/internal/services/groups" "algobot/internal/telegram/handlers/text" mocks2 "algobot/test/mocks" @@ -29,7 +30,7 @@ func TestMissingKids(t *testing.T) { t.Run("happy path", func(t *testing.T) { gomock.InOrder( actualGroup.EXPECT().CurrentGroup(int64(1), gomock.Any(), "").Return(acGroupasset, nil).Times(1), - mctx.EXPECT().Send("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", telebot.ModeMarkdown).Return(nil).Times(1), + mctx.EXPECT().Send("Группа: title\nЛекция: lesson\n\nОбщее число детей: 3\nОтсутствуют: 2\n\n```Отсутствующие\n1 (Уже 2 занятие)\n1\n```", keyboards.MissingKids(1, 1), telebot.ModeMarkdown).Return(nil).Times(1), ) err := handler(mctx) assert.NoError(t, err) From bc126fef84a396820218e7cb96e5055d8008f05a Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 09:30:07 +0300 Subject: [PATCH 37/44] add service get credentials --- internal/app/telegram/app.go | 1 + internal/domain/models/creds.go | 7 +++ internal/services/backoffice/creds.go | 37 +++++++++++++ .../telegram/handlers/callback/getCreds.go | 52 ++++++++++++++++++ test/mocks/telegram/handlers/mockgen.go | 1 + test/services/backoffice_test.go | 36 +++++++++++++ test/telegram/handlers/getCreds_test.go | 54 +++++++++++++++++++ 7 files changed, 188 insertions(+) create mode 100644 internal/domain/models/creds.go create mode 100644 internal/services/backoffice/creds.go create mode 100644 internal/telegram/handlers/callback/getCreds.go create mode 100644 test/telegram/handlers/getCreds_test.go diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index c5e5f3e..ecabc97 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -100,6 +100,7 @@ func New( r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(storage, log)) r.HandleFuncCallback("\frefresh_groups", callback.RefreshGroup(groupServ, log)) + r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fget_creds__(.+)$`), callback.GetCreds()) r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fclose_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.CloseLesson, log)) r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fopen_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.OpenLesson, log)) }) diff --git a/internal/domain/models/creds.go b/internal/domain/models/creds.go new file mode 100644 index 0000000..96893f5 --- /dev/null +++ b/internal/domain/models/creds.go @@ -0,0 +1,7 @@ +package models + +type Credential struct { + Fullname string + Login string + Password string +} diff --git a/internal/services/backoffice/creds.go b/internal/services/backoffice/creds.go new file mode 100644 index 0000000..dd83a72 --- /dev/null +++ b/internal/services/backoffice/creds.go @@ -0,0 +1,37 @@ +package backoffice + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "log/slog" +) + +func (bo *Backoffice) Creds(uid int64, groupID string, traceID interface{}) ([]models.Credential, error) { + const op = "services.backoffice.Creds" + log := bo.log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + + cookie, err := bo.cookieGetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return nil, fmt.Errorf("%s failed to get cookies: %w", op, err) + } + group, err := bo.groupView.KidsNamesByGroup(groupID, cookie) + if err != nil { + return nil, fmt.Errorf("%s failed to get KidsNamesByGroup: %w", op, err) + } + + creds := make([]models.Credential, len(group.Data.Items)) + for i, item := range group.Data.Items { + creds[i] = models.Credential{ + Fullname: item.FullName, + Login: item.Username, + Password: item.Password, + } + } + + return creds, nil +} diff --git a/internal/telegram/handlers/callback/getCreds.go b/internal/telegram/handlers/callback/getCreds.go new file mode 100644 index 0000000..3267761 --- /dev/null +++ b/internal/telegram/handlers/callback/getCreds.go @@ -0,0 +1,52 @@ +package callback + +import ( + "algobot/internal/domain/models" + "algobot/internal/lib/logger/sl" + "fmt" + "gopkg.in/telebot.v4" + "log/slog" + "strings" +) + +type GetterCreds interface { + Creds(uid int64, groupID string, traceID interface{}) ([]models.Credential, error) +} + +func GetCreds(creds GetterCreds, log *slog.Logger) telebot.HandlerFunc { + return func(ctx telebot.Context) error { + const op = "callback.GetCreds" + + traceID := ctx.Get("trace_id") + log := log.With( + slog.String("op", op), + slog.Any("trace_id", traceID), + ) + uid := ctx.Sender().ID + + groupID := strings.TrimPrefix(ctx.Callback().Data, "\fget_creds_") + + c, err := creds.Creds(uid, groupID, traceID) + if err != nil { + log.Warn("error while get creds", sl.Err(err)) + return fmt.Errorf("%s error while get creds: %w", op, err) + } + + return ctx.Send(getCredsMsg(c), telebot.ModeHTML) + } +} + +func getCredsMsg(c []models.Credential) string { + sb := strings.Builder{} + for _, credential := range c { + sb.WriteString("") + sb.WriteString(credential.Fullname) + sb.WriteString("") + sb.WriteString(" - ") + sb.WriteString(credential.Login) + sb.WriteString(" : ") + sb.WriteString(credential.Password) + sb.WriteString("\n") + } + return sb.String() +} diff --git a/test/mocks/telegram/handlers/mockgen.go b/test/mocks/telegram/handlers/mockgen.go index 8da607a..860afee 100644 --- a/test/mocks/telegram/handlers/mockgen.go +++ b/test/mocks/telegram/handlers/mockgen.go @@ -27,3 +27,4 @@ package mocks //go:generate mockgen -destination=./actualGroup_mock.go -package=mocks algobot/internal/telegram/handlers/text ActualGroup //go:generate mockgen -destination=./lessonStatuser_mock.go -package=mocks algobot/internal/telegram/handlers/callback LessonStatuser +//go:generate mockgen -destination=./getterCreds_mock.go -package=mocks algobot/internal/telegram/handlers/callback GetterCreds diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 7eca6cc..96d66d1 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -194,6 +194,42 @@ func TestBackoffice(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) + t.Run("Creds", func(t *testing.T) { + ID := int64(1) + cookie := "cookie" + groupID := "id" + errExp := errors.New("") + t.Run("HappyPath", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(ID).Return(cookie, nil).Times(1), + groupView.EXPECT().KidsNamesByGroup(groupID, cookie).Return(kidsNamesByGroupBackoffice, nil).Times(1), + ) + creds, err := sbo.Creds(ID, groupID, "") + assert.NoError(t, err) + assert.Equal(t, []models.Credential{ + { + Fullname: "Иван Иванов", + Login: "ivan101", + Password: "securepassword123", + }, + }, creds) + }) + t.Run("Cookie return err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(ID).Return("", errExp).Times(1), + ) + _, err := sbo.Creds(ID, groupID, "") + assert.ErrorIs(t, err, errExp) + }) + t.Run("KidsNamesByGroup return err", func(t *testing.T) { + gomock.InOrder( + cookieGetter.EXPECT().Cookies(ID).Return(cookie, nil).Times(1), + groupView.EXPECT().KidsNamesByGroup(groupID, cookie).Return(backoffice2.NamesByGroup{}, errExp).Times(1), + ) + _, err := sbo.Creds(ID, groupID, "") + assert.ErrorIs(t, err, errExp) + }) + }) } var KidViewBackoffice = backoffice2.KidView{ diff --git a/test/telegram/handlers/getCreds_test.go b/test/telegram/handlers/getCreds_test.go new file mode 100644 index 0000000..39f7583 --- /dev/null +++ b/test/telegram/handlers/getCreds_test.go @@ -0,0 +1,54 @@ +package test + +import ( + "algobot/internal/domain/models" + "algobot/internal/telegram/handlers/callback" + "algobot/test/mocks" + mocks3 "algobot/test/mocks/telegram" + mocks2 "algobot/test/mocks/telegram/handlers" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" +) + +func TestGetterCreds(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + creds := mocks2.NewMockGetterCreds(ctrl) + mctx := mocks3.NewMockContext(ctrl) + + handler := callback.GetCreds(creds, log) + + mctx.EXPECT().Get(gomock.Any()).Return("").AnyTimes() + mctx.EXPECT().Sender().Return(&telebot.User{ID: 1}).AnyTimes() + + t.Run("happy path", func(t *testing.T) { + gomock.InOrder( + mctx.EXPECT().Callback().Return(&telebot.Callback{Data: "\fget_creds_123"}), + creds.EXPECT().Creds(int64(1), "123", "").Return([]models.Credential{ + { + Fullname: "f", + Login: "l", + Password: "p", + }, + }, nil).Times(1), + mctx.EXPECT().Send("f - l : p\n", telebot.ModeHTML).Return(nil).Times(1), + ) + err := handler(mctx) + assert.NoError(t, err) + }) + t.Run("creds return err", func(t *testing.T) { + errExp := errors.New("err") + + gomock.InOrder( + mctx.EXPECT().Callback().Return(&telebot.Callback{Data: "\fget_creds_123"}), + creds.EXPECT().Creds(int64(1), "123", "").Return(nil, errExp).Times(1), + ) + err := handler(mctx) + assert.ErrorIs(t, err, errExp) + }) +} From 1d7f5dd7b8b2913722d06e9fc444285751a5f619 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 09:34:15 +0300 Subject: [PATCH 38/44] fix handler test --- test/services/backoffice_test.go | 2 +- test/telegram/handlers/lessonStatus_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 96d66d1..5744e15 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -194,7 +194,7 @@ func TestBackoffice(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) - t.Run("Creds", func(t *testing.T) { + t.Run("Cred+s", func(t *testing.T) { ID := int64(1) cookie := "cookie" groupID := "id" diff --git a/test/telegram/handlers/lessonStatus_test.go b/test/telegram/handlers/lessonStatus_test.go index 94fe01b..93f17a5 100644 --- a/test/telegram/handlers/lessonStatus_test.go +++ b/test/telegram/handlers/lessonStatus_test.go @@ -64,7 +64,7 @@ func TestLessonStatus(t *testing.T) { handler := callback.LessonStatus(ls, backoffice.CloseLesson, log) gomock.InOrder( - mctx.EXPECT().Callback().Return(&tele.Callback{Data: "close_lesson_1_1"}), + mctx.EXPECT().Callback().Return(&tele.Callback{Data: "\fclose_lesson_1_1"}), ls.EXPECT().SetLessonStatus(int64(1), "1", "1", backoffice.CloseLesson, "").Return(errExp).Times(1), ) From b2e0807e377a6269038ed2c5be34ae1fd53f8870 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 12:04:20 +0300 Subject: [PATCH 39/44] add message scheduler --- cmd/algobot/main.go | 5 +- config/dev.yaml | 3 +- internal/app/app.go | 10 +- internal/app/scheduler/app.go | 102 ++++++++++++++++++ internal/app/telegram/app.go | 16 ++- internal/config/config.go | 1 + internal/domain/models/user.go | 9 ++ internal/domain/scheduler/message.go | 11 ++ internal/lib/backoffice/kidsMessages.go | 32 ++++++ internal/services/backoffice/backoffice.go | 5 +- internal/services/backoffice/messagesUser.go | 100 +++++++++++++++++ internal/services/schedule/schedule.go | 56 ++++++++++ internal/storage/sqlite/chaneNotifDate.go | 20 ++++ .../storage/sqlite/usersByNotification.go | 54 ++++++++++ internal/telegram/handlers/text/settings.go | 2 +- test/app/schedule_test.go | 58 ++++++++++ test/lib/backoffice/backoffice_test.go | 33 ++++++ test/lib/backoffice/kidsMessage_example | 19 ++++ test/mocks/app/mockgen.go | 4 + test/mocks/services/mockgen.go | 3 + test/services/backoffice_test.go | 55 +++++++++- test/services/schedule_test.go | 51 +++++++++ .../03_append_user_in_users_table.sql | 6 +- test/storage/sqlite_test.go | 24 +++++ 24 files changed, 660 insertions(+), 19 deletions(-) create mode 100644 internal/app/scheduler/app.go create mode 100644 internal/domain/models/user.go create mode 100644 internal/domain/scheduler/message.go create mode 100644 internal/lib/backoffice/kidsMessages.go create mode 100644 internal/services/backoffice/messagesUser.go create mode 100644 internal/services/schedule/schedule.go create mode 100644 internal/storage/sqlite/chaneNotifDate.go create mode 100644 internal/storage/sqlite/usersByNotification.go create mode 100644 test/app/schedule_test.go create mode 100644 test/lib/backoffice/kidsMessage_example create mode 100644 test/mocks/app/mockgen.go create mode 100644 test/services/schedule_test.go diff --git a/cmd/algobot/main.go b/cmd/algobot/main.go index cff4896..7a2089b 100644 --- a/cmd/algobot/main.go +++ b/cmd/algobot/main.go @@ -24,8 +24,8 @@ func main() { go application.TelegramBot.Run() log.Info("started telegram bot") - // TODO : start bot app - // TODO : start message scheduler app + go application.Scheduler.Run() + log.Info("starting msg scheduler") // graceful shutdown ch := make(chan os.Signal, 1) @@ -33,6 +33,7 @@ func main() { <-ch log.Info("shutting down application") application.TelegramBot.Stop() + application.Scheduler.Stop() log.Info("application gracefully stopped") } diff --git a/config/dev.yaml b/config/dev.yaml index 94298e3..8648790 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -1,6 +1,6 @@ env: local # or prod storage_path: "./storage/storage.db" -# telegram token set in env variables - TELEGRAM_TOKEN +# telegram_token: TOKEN or set in env variables - TELEGRAM_TOKEN migrations_path: "./migrations" grpc: host: "localhost" @@ -10,6 +10,7 @@ rate_limit: fill_period: 800ms bucket_limit: 6 backoffice: + message_timer: 5m retries: 3 retries_timeout: 5s response_timeout: 15s \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 009b69f..277b23e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,9 +1,11 @@ package app import ( + "algobot/internal/app/scheduler" "algobot/internal/app/telegram" "algobot/internal/config" "algobot/internal/lib/backoffice" + backoffice2 "algobot/internal/services/backoffice" "algobot/internal/storage/sqlite" "log/slog" ) @@ -12,6 +14,7 @@ type App struct { log *slog.Logger cfg *config.Config TelegramBot *telegram.App + Scheduler *scheduler.App } func New(log *slog.Logger, cfg *config.Config) *App { @@ -20,14 +23,19 @@ func New(log *slog.Logger, cfg *config.Config) *App { if err != nil { panic(err) } + bo := backoffice.NewBackoffice(&cfg.Backoffice) + boSvc := backoffice2.NewBackoffice(log, storage, bo, bo, bo, bo) + sch := scheduler.New(log, cfg, storage, boSvc) botApplication := telegram.New( log, cfg, storage, bo, + boSvc, + sch.Chan(), ) - return &App{log: log, cfg: cfg, TelegramBot: botApplication} + return &App{log: log, cfg: cfg, TelegramBot: botApplication, Scheduler: sch} } diff --git a/internal/app/scheduler/app.go b/internal/app/scheduler/app.go new file mode 100644 index 0000000..701fcda --- /dev/null +++ b/internal/app/scheduler/app.go @@ -0,0 +1,102 @@ +package scheduler + +import ( + "algobot/internal/config" + "algobot/internal/domain/models" + "algobot/internal/domain/scheduler" + "algobot/internal/lib/logger/sl" + "log/slog" + "time" +) + +type Domain interface { + UsersByNotification(wantNotif int) ([]models.User, error) + ChaneNotifDate(uid int64, lastnotif string) error +} +type Backoffice interface { + MessagesUser(uid int64, lastTime string) ([]scheduler.Message, error) +} + +type App struct { + ch chan scheduler.Message + cfg *config.Config + ticker *time.Ticker + log *slog.Logger + domain Domain + bo Backoffice +} + +func New(log *slog.Logger, cfg *config.Config, domain Domain, bo Backoffice) *App { + + return &App{ + log: log, + cfg: cfg, + bo: bo, + domain: domain, + ch: make(chan scheduler.Message), + } +} + +func (a *App) Run() { + const op = "scheduler.Run" + log := a.log.With( + slog.String("op", op), + ) + log.Info("start scheduling") + + ticker := time.NewTicker(a.cfg.Backoffice.MessageTimer) + a.ticker = ticker + + go func() { + for { + select { + case <-ticker.C: + a.GetMessage() + } + } + }() +} + +func (a *App) GetMessage() { + // TODO: maybe refactor into other struct + const op = "scheduler.app.GetMessage" + log := a.log.With( + slog.String("op", op), + ) + users, err := a.domain.UsersByNotification(1) + if err != nil { + log.Warn("error while get users by notif", sl.Err(err)) + } + for _, user := range users { + msgs, err := a.bo.MessagesUser(user.Uid, user.LastNotification) + if err != nil { + log.Warn("error while fetch MessagesUser", sl.Err(err)) + continue + } + for _, msg := range msgs { + a.ch <- msg + err := a.domain.ChaneNotifDate(msg.To, msg.Time) + if err != nil { + log.Warn("error while fetch ChaneNotifDate", sl.Err(err)) + } + } + } + +} + +func (a *App) Stop() { + const op = "scheduler.Stop" + log := a.log.With( + slog.String("op", op), + ) + log.Info("stop scheduling") + + if a.ticker != nil { + a.ticker.Stop() + } + close(a.ch) +} + +func (a *App) Chan() chan scheduler.Message { + return a.ch +} diff --git a/internal/app/telegram/app.go b/internal/app/telegram/app.go index ecabc97..9a028e1 100644 --- a/internal/app/telegram/app.go +++ b/internal/app/telegram/app.go @@ -2,6 +2,7 @@ package telegram import ( "algobot/internal/config" + "algobot/internal/domain/scheduler" backoffice3 "algobot/internal/lib/backoffice" "algobot/internal/lib/fsm" "algobot/internal/lib/fsm/memory" @@ -10,6 +11,7 @@ import ( "algobot/internal/services/backoffice" "algobot/internal/services/groups" grpc2 "algobot/internal/services/grpc" + "algobot/internal/services/schedule" "algobot/internal/storage/sqlite" "algobot/internal/telegram/handlers/callback" "algobot/internal/telegram/handlers/text" @@ -33,12 +35,7 @@ type App struct { bot *tele.Bot } -func New( - log *slog.Logger, - cfg *config.Config, - storage *sqlite.Sqlite, - bo *backoffice3.Backoffice, -) *App { +func New(log *slog.Logger, cfg *config.Config, storage *sqlite.Sqlite, bo *backoffice3.Backoffice, boSvc *backoffice.Backoffice, messages chan scheduler.Message) *App { const op = "telegram.New" nlog := log.With( @@ -51,7 +48,7 @@ func New( Timeout: 10 * time.Second, }, OnError: func(e error, c tele.Context) { // TODO : refactor into handler - traceID := c.Get("trace_id") // TODO : maybe send warnings to admin ? + traceID := c.Get("trace_id") c.Send(fmt.Sprintf("[%s]\n\nУпс! Произошла какая-то непредвиденная ошибка!\nОбратитесь к администратору", traceID), tele.ModeHTML) log.Warn("cant handle error", sl.Err(e), slog.Any("trace_id", traceID)) }, @@ -70,7 +67,8 @@ func New( cfg.GRPC, grpc2.WithLogger(log), ) - boSvc := backoffice.NewBackoffice(log, storage, bo, bo, bo) + sch := schedule.NewSchedule(messages, b) + go sch.Process() // initialize routes b.Use(trace.New(log)) @@ -100,7 +98,7 @@ func New( r.HandleFuncCallback("\fchange_notification", callback.NewChangeNotification(storage, log)) r.HandleFuncCallback("\frefresh_groups", callback.RefreshGroup(groupServ, log)) - r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fget_creds__(.+)$`), callback.GetCreds()) + r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fget_creds_(.+)$`), callback.GetCreds(boSvc, log)) r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fclose_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.CloseLesson, log)) r.HandleFuncRegexpCallback(regexp.MustCompile(`^\fopen_lesson_(.+)$`), callback.LessonStatus(boSvc, backoffice.OpenLesson, log)) }) diff --git a/internal/config/config.go b/internal/config/config.go index 26460af..3d10424 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ type Config struct { type Backoffice struct { Retries int `yaml:"retries" env-default:"3"` + MessageTimer time.Duration `yaml:"message_timer" env-default:"5m"` RetriesTimeout time.Duration `yaml:"retries_timeout" env-default:"5s"` ResponseTimeout time.Duration `yaml:"response_timeout" env-default:"15s"` } diff --git a/internal/domain/models/user.go b/internal/domain/models/user.go new file mode 100644 index 0000000..ad18feb --- /dev/null +++ b/internal/domain/models/user.go @@ -0,0 +1,9 @@ +package models + +type User struct { + ID int + Uid int64 + Cookie string + LastNotification string + Notification int +} diff --git a/internal/domain/scheduler/message.go b/internal/domain/scheduler/message.go new file mode 100644 index 0000000..7081574 --- /dev/null +++ b/internal/domain/scheduler/message.go @@ -0,0 +1,11 @@ +package scheduler + +type Message struct { + To int64 + From string + Theme string + Link string + Text string + LinkURL string + Time string +} diff --git a/internal/lib/backoffice/kidsMessages.go b/internal/lib/backoffice/kidsMessages.go new file mode 100644 index 0000000..022ae18 --- /dev/null +++ b/internal/lib/backoffice/kidsMessages.go @@ -0,0 +1,32 @@ +package backoffice + +import ( + "algobot/internal/domain/backoffice" + "encoding/json" + "fmt" +) + +func (bo *Backoffice) KidsMessages(cookie string) (backoffice.KidsMessages, error) { + const op = "backoffice.KidsMessages" + + req, err := bo.createReq("GET", "/api/v1/teacherComment/projects", cookie, map[string]string{ + "from": "0", + "limit": "30", + }, nil) + if err != nil { + return backoffice.KidsMessages{}, fmt.Errorf("%s err create req: %w", op, err) + } + + data, reqErr := bo.doReq(req) + if reqErr != nil { + return backoffice.KidsMessages{}, fmt.Errorf("%s err doReq: %w", op, err) + } + + var response backoffice.KidsMessages + err = json.NewDecoder(data.Body).Decode(&response) + if err != nil { + return backoffice.KidsMessages{}, fmt.Errorf("%s err decode json: %w", op, err) + } + + return response, nil +} diff --git a/internal/services/backoffice/backoffice.go b/internal/services/backoffice/backoffice.go index 1a9c26c..b191908 100644 --- a/internal/services/backoffice/backoffice.go +++ b/internal/services/backoffice/backoffice.go @@ -12,8 +12,9 @@ type Backoffice struct { groupView GroupView kidViewer KidViewer lessonStatuser LessonStatuser + msgFetcher MessageFetcher } -func NewBackoffice(log *slog.Logger, cookieGetter CookieGetter, groupView GroupView, kidViewer KidViewer, lessonStatus LessonStatuser) *Backoffice { - return &Backoffice{log: log, cookieGetter: cookieGetter, groupView: groupView, kidViewer: kidViewer, lessonStatuser: lessonStatus} +func NewBackoffice(log *slog.Logger, cookieGetter CookieGetter, groupView GroupView, kidViewer KidViewer, lessonStatuser LessonStatuser, msgFetcher MessageFetcher) *Backoffice { + return &Backoffice{log: log, cookieGetter: cookieGetter, groupView: groupView, kidViewer: kidViewer, lessonStatuser: lessonStatuser, msgFetcher: msgFetcher} } diff --git a/internal/services/backoffice/messagesUser.go b/internal/services/backoffice/messagesUser.go new file mode 100644 index 0000000..c7c4c8d --- /dev/null +++ b/internal/services/backoffice/messagesUser.go @@ -0,0 +1,100 @@ +package backoffice + +import ( + "algobot/internal/domain/backoffice" + "algobot/internal/domain/scheduler" + "algobot/internal/lib/logger/sl" + "fmt" + "log/slog" + "strings" + "time" +) + +var dateMap = map[string]string{ + "янв": "01", + "февр": "02", + "мар": "03", + "апр": "04", + "мая": "05", + "июн": "06", + "июл": "07", + "авг": "08", + "сент": "09", + "окт": "10", + "нояб": "11", + "дек": "12", +} + +type MessageFetcher interface { + KidsMessages(cookie string) (backoffice.KidsMessages, error) +} + +func (bo *Backoffice) MessagesUser(uid int64, lastTime string) ([]scheduler.Message, error) { + const op = "services.backoffice.MessagesUser" + log := bo.log.With( + slog.String("op", op), + ) + + cookie, err := bo.cookieGetter.Cookies(uid) + if err != nil { + log.Warn("failed to get cookies", sl.Err(err)) + return nil, fmt.Errorf("%s failed to get cookies: %w", op, err) + } + + dateNotif := time.Time{} + if lastTime != "" { + dateNotif = parseDate(lastTime) + } + + messages, err := bo.msgFetcher.KidsMessages(cookie) + if err != nil { + return nil, fmt.Errorf("%s failed to get KidsMessages: %w", op, err) + } + + var msgs []scheduler.Message + for i := len(messages.Data.Projects) - 1; i >= 0; i-- { + if messages.Data.Projects[i].SenderScope == "student" { + if dateNotif.Before(parseDate(messages.Data.Projects[i].LastTime)) { + m := scheduler.Message{ + From: messages.Data.Projects[i].Name, + Theme: messages.Data.Projects[i].Title, + Link: fmt.Sprintf("https://backoffice.algoritmika.org%s", messages.Data.Projects[i].Link), + Text: messages.Data.Projects[i].Content, + Time: messages.Data.Projects[i].LastTime, + To: uid, + } + + if messages.Data.Projects[i].Type == "img" { + m.LinkURL = fmt.Sprintf("https://backoffice.algoritmika.org%s", m.Text) + } + + msgs = append(msgs, m) + } + } + } + + return msgs, nil +} + +func parseDate(lastTime string) time.Time { + parts := strings.Split(lastTime, " ") + + day := parts[0] + month := dateMap[strings.Replace(parts[1], ".", "", -1)] + + if len(parts) == 3 { + timeHour := parts[2] + + retTime, _ := time.Parse("2 01 2006 15:04", fmt.Sprintf("%s %s %d %s", day, month, time.Now().Year(), timeHour)) + return retTime + } + if len(parts) == 4 { + year := strings.Replace(parts[2], "`", "", -1) + year = strings.Replace(year, ",", "", -1) + timeHour := parts[3] + + retTime, _ := time.Parse("2 01 06 15:04", fmt.Sprintf("%s %s %s %s", day, month, year, timeHour)) + return retTime + } + return time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC) +} diff --git a/internal/services/schedule/schedule.go b/internal/services/schedule/schedule.go new file mode 100644 index 0000000..203b5c3 --- /dev/null +++ b/internal/services/schedule/schedule.go @@ -0,0 +1,56 @@ +package schedule + +import ( + "algobot/internal/domain/scheduler" + "fmt" + "gopkg.in/telebot.v4" + "strings" +) + +type Sender interface { + Send(to telebot.Recipient, what interface{}, opts ...interface{}) (*telebot.Message, error) +} + +type Schedule struct { + ch chan scheduler.Message + sender Sender +} + +func NewSchedule(ch chan scheduler.Message, sender Sender) *Schedule { + return &Schedule{ch: ch, sender: sender} +} + +func (s *Schedule) Process() { + for msg := range s.ch { + if msg.LinkURL != "" { + p := &telebot.Photo{File: telebot.FromURL(msg.LinkURL), Caption: getMsg(msg)} + s.sender.Send( + telebot.ChatID(msg.To), + p, + telebot.ModeMarkdown, + telebot.NoPreview, + ) + continue + } + s.sender.Send( + telebot.ChatID(msg.To), + getMsg(msg), + telebot.ModeMarkdown, + telebot.NoPreview, + ) + } +} + +func getMsg(msg scheduler.Message) string { + sb := strings.Builder{} + sb.WriteString("🔔 Новое сообщение\n\n") + sb.WriteString(fmt.Sprintf("От: %s\n", msg.From)) + sb.WriteString(fmt.Sprintf("Тема: %s\n", msg.Theme)) + sb.WriteString(fmt.Sprintf("Ссылка: %s\n\n", msg.Link)) + if msg.LinkURL == "" { + sb.WriteString("```Сообщение:\n") + sb.WriteString(msg.Text) + sb.WriteString("\n```") + } + return sb.String() +} diff --git a/internal/storage/sqlite/chaneNotifDate.go b/internal/storage/sqlite/chaneNotifDate.go new file mode 100644 index 0000000..d38deff --- /dev/null +++ b/internal/storage/sqlite/chaneNotifDate.go @@ -0,0 +1,20 @@ +package sqlite + +import "fmt" + +func (s *Sqlite) ChaneNotifDate(uid int64, lastnotif string) error { + const op = "sqlite.ChaneNotifDate" + + sqlq := "UPDATE users SET last_notification_msg = ? WHERE uid = ?" + pr, err := s.db.Prepare(sqlq) + if err != nil { + return fmt.Errorf("%s error while preparing statement: %w", op, err) + } + defer pr.Close() + + if _, err := pr.Exec(lastnotif, uid); err != nil { + return fmt.Errorf("%s error while executing statement: %w", op, err) + } + + return nil +} diff --git a/internal/storage/sqlite/usersByNotification.go b/internal/storage/sqlite/usersByNotification.go new file mode 100644 index 0000000..987e124 --- /dev/null +++ b/internal/storage/sqlite/usersByNotification.go @@ -0,0 +1,54 @@ +package sqlite + +import ( + "algobot/internal/domain/models" + "database/sql" + "fmt" +) + +func (s *Sqlite) UsersByNotification(wantNotif int) ([]models.User, error) { + const op = "sqlite.UsersByNotification" + + q := `SELECT * FROM users WHERE notification=?` + pr, err := s.db.Prepare(q) + if err != nil { + return nil, fmt.Errorf("%s error while preparing sql: %w", op, err) + } + + row, err := pr.Query(wantNotif) + if err != nil { + return nil, fmt.Errorf("%s error while Query sql: %w", op, err) + } + + var users []models.User + for row.Next() { + var id int + var uid int64 + var cookie sql.NullString + var lastNotif sql.NullString + var notif int + + if err := row.Scan(&id, &uid, &cookie, &lastNotif, ¬if); err != nil { + return nil, fmt.Errorf("%s error while scanning row: %w", op, err) + } + + cookieNew := "" + if cookie.Valid { + cookieNew = cookie.String + } + lastNotifNew := "" + if lastNotif.Valid { + lastNotifNew = lastNotif.String + } + users = append(users, models.User{ + ID: id, + Uid: uid, + Cookie: cookieNew, + LastNotification: lastNotifNew, + Notification: notif, + }) + + } + + return users, nil +} diff --git a/internal/telegram/handlers/text/settings.go b/internal/telegram/handlers/text/settings.go index 8c44b44..8679aa7 100644 --- a/internal/telegram/handlers/text/settings.go +++ b/internal/telegram/handlers/text/settings.go @@ -50,7 +50,7 @@ func GetSettingsMessage(cookies string, notification bool) string { sb.WriteString("✖️") } sb.WriteString("\nУведомление от чата:") - if !notification { + if notification { sb.WriteString("✅") } else { sb.WriteString("✖️") diff --git a/test/app/schedule_test.go b/test/app/schedule_test.go new file mode 100644 index 0000000..3a49183 --- /dev/null +++ b/test/app/schedule_test.go @@ -0,0 +1,58 @@ +package test + +import ( + "algobot/internal/app/scheduler" + "algobot/internal/config" + "algobot/internal/domain/models" + scheduler2 "algobot/internal/domain/scheduler" + "algobot/test/mocks" + mocks2 "algobot/test/mocks/app" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func TestSchedule(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + log := mocks.NewMockLogger() + domain := mocks2.NewMockDomain(ctrl) + bo := mocks2.NewMockBackoffice(ctrl) + cfg := &config.Config{Backoffice: config.Backoffice{ + MessageTimer: 5 * time.Second, + }} + + app := scheduler.New(log, cfg, domain, bo) + + go func() { + msg := <-app.Chan() + assert.Equal(t, assetMSG, msg) + app.Stop() + }() + + domain.EXPECT().UsersByNotification(1).Return([]models.User{ + { + ID: 1, + Uid: 1, + Cookie: "Cookie", + LastNotification: "LastNotification", + Notification: 1, + }, + }, nil).Times(1) + bo.EXPECT().MessagesUser(int64(1), "LastNotification").Return([]scheduler2.Message{assetMSG}, nil).Times(1) + domain.EXPECT().ChaneNotifDate(int64(1), "newTime").Return(nil).Times(1) + + app.GetMessage() +} + +var assetMSG = scheduler2.Message{ + To: 1, + From: "From", + Theme: "Theme", + Link: "Link", + Text: "Text", + LinkURL: "", + Time: "newTime", +} diff --git a/test/lib/backoffice/backoffice_test.go b/test/lib/backoffice/backoffice_test.go index b0eeb24..b36cce4 100644 --- a/test/lib/backoffice/backoffice_test.go +++ b/test/lib/backoffice/backoffice_test.go @@ -150,6 +150,24 @@ func TestBackoffice(t *testing.T) { assert.NoError(t, err) }) }) + t.Run("KidsMessages", func(t *testing.T) { + responseHTML := readFile("kidsMessage_example") + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseHTML)) + rw.WriteHeader(http.StatusOK) + })) + defer server.Close() + + bo := backoffice.NewBackoffice(&config.Backoffice{ + Retries: 5, + RetriesTimeout: time.Second, + ResponseTimeout: time.Second, + }, backoffice.WithURL(server.URL)) + + msgs, err := bo.KidsMessages("") + assert.NoError(t, err) + assert.Equal(t, backofficeMsg, msgs) + }) } func readFile(fileName string) string { @@ -310,3 +328,18 @@ var backofficeKidViewExpected = backoffice2.KidView{ Links: backoffice2.Links{Self: backoffice2.SelfLink{Href: "/student/update/70245813"}}, }, } +var backofficeMsg = backoffice2.KidsMessages{ + Status: "success", + Data: backoffice2.MessagesData{Projects: []backoffice2.Message{{ + UID: "33123098level1123826", + New: false, + SenderID: 42407, + SenderScope: "user", + Type: "text", + Content: "content", + Name: "name", + LastTime: "15 мар. 19:26", + Title: "М5 У2. Игра \"Game\". Ч. 1", + Link: "/task-preview/link", + }}}, +} diff --git a/test/lib/backoffice/kidsMessage_example b/test/lib/backoffice/kidsMessage_example new file mode 100644 index 0000000..b65e43a --- /dev/null +++ b/test/lib/backoffice/kidsMessage_example @@ -0,0 +1,19 @@ +{ + "status": "success", + "data": { + "projects": [ + { + "uid": "33123098level1123826", + "new": false, + "senderId": 42407, + "senderScope": "user", + "type": "text", + "content": "content", + "name": "name", + "lastTime": "15 мар. 19:26", + "title": "М5 У2. Игра \"Game\". Ч. 1", + "link": "/task-preview/link" + } + ] + } +} \ No newline at end of file diff --git a/test/mocks/app/mockgen.go b/test/mocks/app/mockgen.go new file mode 100644 index 0000000..2e054a3 --- /dev/null +++ b/test/mocks/app/mockgen.go @@ -0,0 +1,4 @@ +package mocks + +//go:generate mockgen -destination=./context_mock.go -package=mocks algobot/internal/app/scheduler Domain +//go:generate mockgen -destination=./api_mock.go -package=mocks algobot/internal/app/scheduler Backoffice diff --git a/test/mocks/services/mockgen.go b/test/mocks/services/mockgen.go index e72fae5..3ab9fd2 100644 --- a/test/mocks/services/mockgen.go +++ b/test/mocks/services/mockgen.go @@ -12,3 +12,6 @@ package mocks //go:generate mockgen -destination=./lessonStatuser_mock.go -package=mocks algobot/internal/services/backoffice LessonStatuser //go:generate mockgen -destination=./kidStats_mock.go -package=mocks algobot/internal/services/groups KidStats +//go:generate mockgen -destination=./sender_mock.go -package=mocks algobot/internal/services/schedule Sender + +//go:generate mockgen -destination=./messageFetcher_mock.go -package=mocks algobot/internal/services/backoffice MessageFetcher diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 5744e15..86442c9 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -3,6 +3,7 @@ package test import ( backoffice2 "algobot/internal/domain/backoffice" "algobot/internal/domain/models" + "algobot/internal/domain/scheduler" backoffice3 "algobot/internal/lib/backoffice" "algobot/internal/services/backoffice" "algobot/test/mocks" @@ -24,8 +25,9 @@ func TestBackoffice(t *testing.T) { groupView := mocks2.NewMockGroupView(ctrl) kidViewer := mocks2.NewMockKidViewer(ctrl) lessonStatuser := mocks2.NewMockLessonStatuser(ctrl) + messageFetcher := mocks2.NewMockMessageFetcher(ctrl) - sbo := backoffice.NewBackoffice(log, cookieGetter, groupView, kidViewer, lessonStatuser) + sbo := backoffice.NewBackoffice(log, cookieGetter, groupView, kidViewer, lessonStatuser, messageFetcher) t.Run("KidView", func(t *testing.T) { uid := int64(1) kidID := "1" @@ -230,6 +232,28 @@ func TestBackoffice(t *testing.T) { assert.ErrorIs(t, err, errExp) }) }) + t.Run("MessagesUser", func(t *testing.T) { + UID := int64(1) + lastTime := "14 мар. 19:26" + cookie := "cookie" + gomock.InOrder( + cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), + messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), + ) + + msgs, err := sbo.MessagesUser(int64(1), lastTime) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + assert.Equal(t, scheduler.Message{ + To: int64(1), + From: "name", + Theme: "М5 У2. Игра \"Game\". Ч. 1", + Link: "https://backoffice.algoritmika.org/task-preview/link", + Text: "content", + LinkURL: "", + Time: "15 мар. 19:26", + }, msgs[0]) + }) } var KidViewBackoffice = backoffice2.KidView{ @@ -564,3 +588,32 @@ var expectedGroupView = models.GroupView{ }, }}, } +var mockMsg = backoffice2.KidsMessages{ + Status: "success", + Data: backoffice2.MessagesData{Projects: []backoffice2.Message{ + { + UID: "33123098level1123826", + New: false, + SenderID: 42407, + SenderScope: "student", + Type: "text", + Content: "content", + Name: "name", + LastTime: "15 мар. 19:26", + Title: "М5 У2. Игра \"Game\". Ч. 1", + Link: "/task-preview/link", + }, + { + UID: "33123098level1123826", + New: false, + SenderID: 42407, + SenderScope: "student", + Type: "text", + Content: "content", + Name: "name", + LastTime: "14 мар. 19:26", + Title: "М5 У2. Игра \"Game\". Ч. 1", + Link: "/task-preview/link", + }, + }}, +} diff --git a/test/services/schedule_test.go b/test/services/schedule_test.go new file mode 100644 index 0000000..8b1b619 --- /dev/null +++ b/test/services/schedule_test.go @@ -0,0 +1,51 @@ +package test + +import ( + "algobot/internal/domain/scheduler" + "algobot/internal/services/schedule" + mocks "algobot/test/mocks/services" + "go.uber.org/mock/gomock" + "gopkg.in/telebot.v4" + "testing" + "time" +) + +func TestShedule(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ch := make(chan scheduler.Message, 2) + sender := mocks.NewMockSender(ctrl) + + sch := schedule.NewSchedule(ch, sender) + + ch <- scheduler.Message{ + To: 123, + From: "From", + Theme: "Theme", + Link: "Link", + Text: "Some Text", + LinkURL: "", + } + ch <- scheduler.Message{ + To: 333, + From: "From2", + Theme: "Theme2", + Link: "Link2", + Text: "Some Text2", + LinkURL: "Link2", + } + sender.EXPECT().Send(telebot.ChatID(123), + "🔔 Новое сообщение\n\nОт: From\nТема: Theme\nСсылка: Link\n\n```Сообщение:\nSome Text\n```", + telebot.ModeMarkdown, + telebot.NoPreview) + sender.EXPECT().Send(telebot.ChatID(333), + &telebot.Photo{File: telebot.FromURL("Link2"), Caption: "🔔 Новое сообщение\n\nОт: From2\nТема: Theme2\nСсылка: Link2\n\n"}, + telebot.ModeMarkdown, + telebot.NoPreview) + + go sch.Process() + time.Sleep(100 * time.Millisecond) + close(ch) + +} diff --git a/test/storage/migrations-suite/03_append_user_in_users_table.sql b/test/storage/migrations-suite/03_append_user_in_users_table.sql index 0d5fe0f..13f3965 100644 --- a/test/storage/migrations-suite/03_append_user_in_users_table.sql +++ b/test/storage/migrations-suite/03_append_user_in_users_table.sql @@ -2,10 +2,12 @@ INSERT INTO users (uid, cookie, last_notification_msg, notification) VALUES (1001, 'cookie', NULL, 0), (1000, NULL, NULL, 0), - (999, NULL, NULL, 0); + (999, NULL, NULL, 0), + (998, NULL, NULL, 1), + (997, NULL, NULL, 2); -- 2 is only for test -- +goose Down DELETE FROM users -WHERE uid in (1001, 1000, 999) \ No newline at end of file +WHERE uid in (1001, 1000, 999, 998, 997) \ No newline at end of file diff --git a/test/storage/sqlite_test.go b/test/storage/sqlite_test.go index a6e093d..33f01d1 100644 --- a/test/storage/sqlite_test.go +++ b/test/storage/sqlite_test.go @@ -167,4 +167,28 @@ func TestSqlite(t *testing.T) { assert.Len(t, groups, 1) assert.Equal(t, gr, groups) }) + t.Run("UsersByNotification", func(t *testing.T) { + users, err := sqlite.UsersByNotification(1) + assert.NoError(t, err) + assert.Len(t, users, 1) + assert.Equal(t, []models.User{ + { + ID: 4, + Uid: 998, + Cookie: "", + LastNotification: "", + Notification: 1, + }, + }, users) + }) + t.Run("ChaneNotifDate", func(t *testing.T) { + users, err := sqlite.UsersByNotification(2) + assert.Equal(t, "", users[0].LastNotification) + + err = sqlite.ChaneNotifDate(997, "bruh") + assert.NoError(t, err) + + users, err = sqlite.UsersByNotification(2) + assert.Equal(t, "bruh", users[0].LastNotification) + }) } From 6bab0e96cf1ccd65b209359f17c97212388cff44 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 13:01:26 +0300 Subject: [PATCH 40/44] change set message logic if empty --- internal/services/backoffice/messagesUser.go | 34 +++++- test/services/backoffice_test.go | 105 +++++++++++++++---- 2 files changed, 114 insertions(+), 25 deletions(-) diff --git a/internal/services/backoffice/messagesUser.go b/internal/services/backoffice/messagesUser.go index c7c4c8d..8cc34ca 100644 --- a/internal/services/backoffice/messagesUser.go +++ b/internal/services/backoffice/messagesUser.go @@ -24,6 +24,20 @@ var dateMap = map[string]string{ "нояб": "11", "дек": "12", } +var dateReverseMap = map[int]string{ + 1: "янв", + 2: "февр", + 3: "мар", + 4: "апр", + 5: "мая", + 6: "июн", + 7: "июл", + 8: "авг", + 9: "сент", + 10: "окт", + 11: "нояб", + 12: "дек", +} type MessageFetcher interface { KidsMessages(cookie string) (backoffice.KidsMessages, error) @@ -41,16 +55,26 @@ func (bo *Backoffice) MessagesUser(uid int64, lastTime string) ([]scheduler.Mess return nil, fmt.Errorf("%s failed to get cookies: %w", op, err) } - dateNotif := time.Time{} - if lastTime != "" { - dateNotif = parseDate(lastTime) - } - messages, err := bo.msgFetcher.KidsMessages(cookie) if err != nil { return nil, fmt.Errorf("%s failed to get KidsMessages: %w", op, err) } + var dateNotif time.Time + if lastTime != "" { + dateNotif = parseDate(lastTime) + } else { + timeNow := time.Now() + return []scheduler.Message{{ + From: "", + Theme: "", + Link: "", + Text: "", + Time: timeNow.Format(fmt.Sprintf("2 %s. 15:04", dateReverseMap[int(timeNow.Month())])), + To: uid, + }}, nil + } + var msgs []scheduler.Message for i := len(messages.Data.Projects) - 1; i >= 0; i-- { if messages.Data.Projects[i].SenderScope == "student" { diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 86442c9..c525226 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -3,12 +3,13 @@ package test import ( backoffice2 "algobot/internal/domain/backoffice" "algobot/internal/domain/models" - "algobot/internal/domain/scheduler" + scheduler2 "algobot/internal/domain/scheduler" backoffice3 "algobot/internal/lib/backoffice" "algobot/internal/services/backoffice" "algobot/test/mocks" mocks2 "algobot/test/mocks/services" "errors" + "fmt" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "strconv" @@ -233,29 +234,81 @@ func TestBackoffice(t *testing.T) { }) }) t.Run("MessagesUser", func(t *testing.T) { - UID := int64(1) - lastTime := "14 мар. 19:26" - cookie := "cookie" - gomock.InOrder( - cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), - messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), - ) + t.Run("happy path", func(t *testing.T) { + UID := int64(1) + lastTime := "14 мар. 19:26" + cookie := "cookie" + gomock.InOrder( + cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), + messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), + ) - msgs, err := sbo.MessagesUser(int64(1), lastTime) - assert.NoError(t, err) - assert.Len(t, msgs, 1) - assert.Equal(t, scheduler.Message{ - To: int64(1), - From: "name", - Theme: "М5 У2. Игра \"Game\". Ч. 1", - Link: "https://backoffice.algoritmika.org/task-preview/link", - Text: "content", - LinkURL: "", - Time: "15 мар. 19:26", - }, msgs[0]) + msgs, err := sbo.MessagesUser(int64(1), lastTime) + assert.NoError(t, err) + assert.Len(t, msgs, 2) + assert.Equal(t, []scheduler2.Message{ + { + To: 1, + From: "name", + Theme: "М5 У2. Игра \"Game\". Ч. 1", + Link: "https://backoffice.algoritmika.org/task-preview/link", + Text: "content", + LinkURL: "", + Time: "15 мар. 19:26", + }, + { + To: 1, + From: "name", + Theme: "М5 У2. Игра \"Game\". Ч. 1", + Link: "https://backoffice.algoritmika.org/task-preview/link", + Text: "content", + LinkURL: "", + Time: "16 мар. 19:26", + }}, msgs) + }) + t.Run("if lat notif empty", func(t *testing.T) { + UID := int64(1) + lastTime := "" + cookie := "cookie" + gomock.InOrder( + cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), + messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), + ) + + msgs, err := sbo.MessagesUser(int64(1), lastTime) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + + timeNow := time.Now() + assert.Contains(t, []scheduler2.Message{ + { + To: 1, + From: "", + Theme: "", + Link: "", + Text: "", + LinkURL: "", + Time: timeNow.Format(fmt.Sprintf("2 %s. 15:04", dateReverseMap[int(timeNow.Month())])), + }}, msgs[0]) + }) }) + } +var dateReverseMap = map[int]string{ + 1: "янв", + 2: "февр", + 3: "мар", + 4: "апр", + 5: "мая", + 6: "июн", + 7: "июл", + 8: "авг", + 9: "сент", + 10: "окт", + 11: "нояб", + 12: "дек", +} var KidViewBackoffice = backoffice2.KidView{ Status: "Активен", Data: backoffice2.Student{ @@ -591,6 +644,18 @@ var expectedGroupView = models.GroupView{ var mockMsg = backoffice2.KidsMessages{ Status: "success", Data: backoffice2.MessagesData{Projects: []backoffice2.Message{ + { + UID: "33123098level1123826", + New: false, + SenderID: 42407, + SenderScope: "student", + Type: "text", + Content: "content", + Name: "name", + LastTime: "16 мар. 19:26", + Title: "М5 У2. Игра \"Game\". Ч. 1", + Link: "/task-preview/link", + }, { UID: "33123098level1123826", New: false, From a340548c29e735f1470ab088840cd7c6f6646b60 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 13:01:26 +0300 Subject: [PATCH 41/44] change set message logic if empty --- internal/services/backoffice/messagesUser.go | 34 +++++- internal/services/schedule/schedule.go | 3 + test/services/backoffice_test.go | 105 +++++++++++++++---- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/internal/services/backoffice/messagesUser.go b/internal/services/backoffice/messagesUser.go index c7c4c8d..8cc34ca 100644 --- a/internal/services/backoffice/messagesUser.go +++ b/internal/services/backoffice/messagesUser.go @@ -24,6 +24,20 @@ var dateMap = map[string]string{ "нояб": "11", "дек": "12", } +var dateReverseMap = map[int]string{ + 1: "янв", + 2: "февр", + 3: "мар", + 4: "апр", + 5: "мая", + 6: "июн", + 7: "июл", + 8: "авг", + 9: "сент", + 10: "окт", + 11: "нояб", + 12: "дек", +} type MessageFetcher interface { KidsMessages(cookie string) (backoffice.KidsMessages, error) @@ -41,16 +55,26 @@ func (bo *Backoffice) MessagesUser(uid int64, lastTime string) ([]scheduler.Mess return nil, fmt.Errorf("%s failed to get cookies: %w", op, err) } - dateNotif := time.Time{} - if lastTime != "" { - dateNotif = parseDate(lastTime) - } - messages, err := bo.msgFetcher.KidsMessages(cookie) if err != nil { return nil, fmt.Errorf("%s failed to get KidsMessages: %w", op, err) } + var dateNotif time.Time + if lastTime != "" { + dateNotif = parseDate(lastTime) + } else { + timeNow := time.Now() + return []scheduler.Message{{ + From: "", + Theme: "", + Link: "", + Text: "", + Time: timeNow.Format(fmt.Sprintf("2 %s. 15:04", dateReverseMap[int(timeNow.Month())])), + To: uid, + }}, nil + } + var msgs []scheduler.Message for i := len(messages.Data.Projects) - 1; i >= 0; i-- { if messages.Data.Projects[i].SenderScope == "student" { diff --git a/internal/services/schedule/schedule.go b/internal/services/schedule/schedule.go index 203b5c3..b71d36a 100644 --- a/internal/services/schedule/schedule.go +++ b/internal/services/schedule/schedule.go @@ -22,6 +22,9 @@ func NewSchedule(ch chan scheduler.Message, sender Sender) *Schedule { func (s *Schedule) Process() { for msg := range s.ch { + if msg.From == "" { + continue + } if msg.LinkURL != "" { p := &telebot.Photo{File: telebot.FromURL(msg.LinkURL), Caption: getMsg(msg)} s.sender.Send( diff --git a/test/services/backoffice_test.go b/test/services/backoffice_test.go index 86442c9..c525226 100644 --- a/test/services/backoffice_test.go +++ b/test/services/backoffice_test.go @@ -3,12 +3,13 @@ package test import ( backoffice2 "algobot/internal/domain/backoffice" "algobot/internal/domain/models" - "algobot/internal/domain/scheduler" + scheduler2 "algobot/internal/domain/scheduler" backoffice3 "algobot/internal/lib/backoffice" "algobot/internal/services/backoffice" "algobot/test/mocks" mocks2 "algobot/test/mocks/services" "errors" + "fmt" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "strconv" @@ -233,29 +234,81 @@ func TestBackoffice(t *testing.T) { }) }) t.Run("MessagesUser", func(t *testing.T) { - UID := int64(1) - lastTime := "14 мар. 19:26" - cookie := "cookie" - gomock.InOrder( - cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), - messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), - ) + t.Run("happy path", func(t *testing.T) { + UID := int64(1) + lastTime := "14 мар. 19:26" + cookie := "cookie" + gomock.InOrder( + cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), + messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), + ) - msgs, err := sbo.MessagesUser(int64(1), lastTime) - assert.NoError(t, err) - assert.Len(t, msgs, 1) - assert.Equal(t, scheduler.Message{ - To: int64(1), - From: "name", - Theme: "М5 У2. Игра \"Game\". Ч. 1", - Link: "https://backoffice.algoritmika.org/task-preview/link", - Text: "content", - LinkURL: "", - Time: "15 мар. 19:26", - }, msgs[0]) + msgs, err := sbo.MessagesUser(int64(1), lastTime) + assert.NoError(t, err) + assert.Len(t, msgs, 2) + assert.Equal(t, []scheduler2.Message{ + { + To: 1, + From: "name", + Theme: "М5 У2. Игра \"Game\". Ч. 1", + Link: "https://backoffice.algoritmika.org/task-preview/link", + Text: "content", + LinkURL: "", + Time: "15 мар. 19:26", + }, + { + To: 1, + From: "name", + Theme: "М5 У2. Игра \"Game\". Ч. 1", + Link: "https://backoffice.algoritmika.org/task-preview/link", + Text: "content", + LinkURL: "", + Time: "16 мар. 19:26", + }}, msgs) + }) + t.Run("if lat notif empty", func(t *testing.T) { + UID := int64(1) + lastTime := "" + cookie := "cookie" + gomock.InOrder( + cookieGetter.EXPECT().Cookies(UID).Return(cookie, nil).Times(1), + messageFetcher.EXPECT().KidsMessages(cookie).Return(mockMsg, nil).Times(1), + ) + + msgs, err := sbo.MessagesUser(int64(1), lastTime) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + + timeNow := time.Now() + assert.Contains(t, []scheduler2.Message{ + { + To: 1, + From: "", + Theme: "", + Link: "", + Text: "", + LinkURL: "", + Time: timeNow.Format(fmt.Sprintf("2 %s. 15:04", dateReverseMap[int(timeNow.Month())])), + }}, msgs[0]) + }) }) + } +var dateReverseMap = map[int]string{ + 1: "янв", + 2: "февр", + 3: "мар", + 4: "апр", + 5: "мая", + 6: "июн", + 7: "июл", + 8: "авг", + 9: "сент", + 10: "окт", + 11: "нояб", + 12: "дек", +} var KidViewBackoffice = backoffice2.KidView{ Status: "Активен", Data: backoffice2.Student{ @@ -591,6 +644,18 @@ var expectedGroupView = models.GroupView{ var mockMsg = backoffice2.KidsMessages{ Status: "success", Data: backoffice2.MessagesData{Projects: []backoffice2.Message{ + { + UID: "33123098level1123826", + New: false, + SenderID: 42407, + SenderScope: "student", + Type: "text", + Content: "content", + Name: "name", + LastTime: "16 мар. 19:26", + Title: "М5 У2. Игра \"Game\". Ч. 1", + Link: "/task-preview/link", + }, { UID: "33123098level1123826", New: false, From 6c46b0bc0d92db2221734e89ff3ef59d91653911 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 16:23:36 +0300 Subject: [PATCH 42/44] add readme --- .github/workflows/workflow.yml | 14 ++++- README.md | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 README.md diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 67a367d..4812e36 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -26,12 +26,20 @@ jobs: - name: Add icon and build binary run: | - cd ./cmd + cd ./cmd/algobot rsrc -ico ./icon.ico GOOS=windows GOARCH=amd64 go build -o ./algobot.exe + - name: build binary migrator + run: | + cd ./cmd/migrator + GOOS=windows GOARCH=amd64 go build -o ./migrator.exe + - name: Upload Windows binary uses: actions/upload-artifact@v4 with: - name: algobot - path: ./cmd/algobot.exe + name: binary + path: | + ./cmd/migrator/migrator.exe + ./cmd/algobot/algobot.exe + ./config/dev.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..f317a9d --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Algobot + +## Описание +**Алгобот** — это Telegram-бот, предназначенный для помощи в ведении учительной деятельности Алгоритмика + +Репозиторий состоит из двух приложений: +- `algobot` — основной Telegram-бот +- `migrator` — утилита для применения миграций с помощью [Goose](https://github.com/pressly/goose) + +Бот использует: +- [gpt4free-grpc-gateway](https://github.com/LZTD1/gpt4free-grpc-gateway) — gRPC-сервер для обращения к нейросетям +- [telebot-context-router](https://github.com/LZTD1/telebot-context-router) — роутер для удобного управления обработчиками сообщений в Telebot + +--- + +## Структура проекта +``` +cmd/ +├── algobot/ # Точка входа для Telegram-бота +│ └── main.go +├── migrator/ # Миграции SQLite базы через Goose +│ └── main.go + +config/ # Конфигурация +migrations/ # SQL миграции +storage/ # SQLite база данных +protos/ # gRPC proto-файлы +``` + +--- + +## Конфигурация + +Файл `config.yaml`: +```yaml +env: local # или prod +storage_path: "./storage/storage.db" # путь то файла базы данных +# telegram_token можно указать в .env или через переменные окружения TELEGRAM_TOKEN +migrations_path: "./migrations" # путь до папки с миграциями +grpc: # настройки grpc + host: "localhost" # адрес grpc сервера с нейронкой + port: 50051 # порт grpc сервера + timeout: 300s # таймаут ответа +rate_limit: # rate limit для запросов в бота + fill_period: 800ms # период пополнение бакета + bucket_limit: 6 # величина бакета +backoffice: # конфигурация для работы с бэкофисом + message_timer: 5m # период опроса новых сообщений от учеников + retries: 3 # количество ретраев при ошибке от сервера + retries_timeout: 5s # ожидание между ретраями + response_timeout: 15s # ожидание ответа от сервера +``` + +Можно передать путь до файла конфига через аргумент `--config` или переменную окружения `CONFIG_PATH`. + +--- + +## Запуск + +### 1. Сборка +```bash +go build -o migrator ./cmd/migrator +go build -o algobot ./cmd/algobot +``` + +### 2. Применение миграций +```bash +./migrator --migrations-path=./migrations --storage-path=./storage/storage.db +``` + +### 3. Запуск бота +```bash +./algobot --config ./config/config.yaml +``` + + +--- + +## Makefile команды +Для удобства сборки и генерации предусмотрен Makefile: + +| Цель | Описание | +|------------------|-----------------------------------------------------------------| +| `make gen` | Генерация Go и gRPC кода из `.proto` файлов (`./protos`) | +| `make grpc-gen` | То же самое, альтернатива `make gen` | +| `make dev` | Запуск бота в dev-режиме с конфигом `./config/local.yaml` | +| `make mock-gen` | Генерация mock'ов в папке `test/` | +| `make migrate` | Запуск миграций через `cmd/migrator` | + +Пример: +```bash +make migrate +make dev +``` + +--- + +## Переменные окружения +| Название | Описание | +|-------------------|-------------------------------| +| `TELEGRAM_TOKEN` | Токен Telegram-бота | +| `CONFIG_PATH` | Путь к файлу конфигурации | From a1c7ae7cffc1ea692c12385b06630792d7046c73 Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 16:25:06 +0300 Subject: [PATCH 43/44] fix pipeline --- .github/workflows/workflow.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 4812e36..58ebb8e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -21,13 +21,9 @@ jobs: with: go-version: '1.23.3' - - name: Install rsrc - run: go install github.com/akavel/rsrc@latest - - name: Add icon and build binary run: | cd ./cmd/algobot - rsrc -ico ./icon.ico GOOS=windows GOARCH=amd64 go build -o ./algobot.exe - name: build binary migrator From 480e9fcf51947f672990bbfac97de711ac07768d Mon Sep 17 00:00:00 2001 From: pavlov Date: Mon, 14 Apr 2025 16:30:26 +0300 Subject: [PATCH 44/44] go mod tidy --- README.md | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f317a9d..3f1ddf2 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,12 @@ go build -o algobot ./cmd/algobot ### 2. Применение миграций ```bash -./migrator --migrations-path=./migrations --storage-path=./storage/storage.db +./migrator -migrations-path=./migrations -storage-path=./storage/storage.db ``` ### 3. Запуск бота ```bash -./algobot --config ./config/config.yaml +./algobot -config ./config/config.yaml ``` diff --git a/go.mod b/go.mod index 0abd098..b5a6721 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( github.com/LZTD1/telebot-context-router v1.1.0 + github.com/PuerkitoBio/goquery v1.10.2 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jxskiss/base62 v1.1.0 @@ -19,7 +20,6 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect - github.com/PuerkitoBio/goquery v1.10.2 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect