diff --git a/cmd/main.go b/cmd/main.go
index 95ab5f1..66559ca 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -5,8 +5,9 @@ import (
"gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/config"
"gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/infrastructure/botService"
- "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/infrastructure/httpClient"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/infrastructure/grpcClient"
"gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/pkg/logger"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/pkg/minioDB"
"gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/pkg/pgDB"
)
@@ -29,11 +30,22 @@ func main() {
log.Fatalln("Error when connect to PostgreSQL: ", err)
}
defer pgDB.DB.Close()
+ // Connect to MinIO
+ minioDB, err := minioDB.New(
+ config.Config.Minio.Endpoint, config.Config.Minio.User,
+ config.Config.Minio.Password, config.Config.Minio.SSLmode,
+ )
+ if err != nil {
+ log.Fatalln("Error when connect to MinIO: ", err)
+ }
// Init support API HTTP client
- supportAPI := httpClient.Init(logger, config.Config.Integrations.SupportApiUrl, config.Config.Tokens.SupportApiKey)
+ supportAPI, err := grpcClient.Init(config.Config.Integrations.SupportApiUrl, config.Config.Tokens.SupportApiKey)
+ if err != nil {
+ log.Fatalln("Error when connect to Support API by gRPC: ", err)
+ }
defer supportAPI.Close()
// Start Telegram Bot service
- if err := botService.Start(config.Config.Bot, logger, pgDB, supportAPI); err != nil {
+ if err := botService.Start(config.Config.Bot, logger, pgDB, minioDB, supportAPI); err != nil {
log.Fatalln("Error when start the Telegram Bot: ", err)
}
}
diff --git a/configs/.env.example b/configs/.env.example
index befcafd..053d476 100644
--- a/configs/.env.example
+++ b/configs/.env.example
@@ -14,3 +14,8 @@ POSTGRE_USER=example
POSTGRE_PASSWORD=example
POSTGRE_NAME=example
POSTGRE_SSLMODE=false
+
+MINIO_ENDPOINT=minio:9000
+MINIO_USER=example
+MINIO_PASSWORD=example
+MINIO_SSLMODE=false
diff --git a/go.mod b/go.mod
index a46930d..797133b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,18 +1,50 @@
module gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService
-go 1.23.1
+go 1.24.0
require (
github.com/caarlos0/env/v11 v11.3.1
+ github.com/gen2brain/heic v0.4.5
+ github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
- gopkg.in/telebot.v4 v4.0.0-beta.4
+ github.com/minio/minio-go/v7 v7.0.95
+ github.com/pdfcpu/pdfcpu v0.11.0
+ gopkg.in/telebot.v4 v4.0.0-beta.5
)
require (
+ gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/ebitengine/purego v0.8.3 // indirect
+ github.com/go-ini/ini v1.67.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/hhrutter/lzw v1.0.0 // indirect
+ github.com/hhrutter/pkcs7 v0.2.0 // indirect
+ github.com/hhrutter/tiff v1.0.2 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/minio/crc64nvme v1.1.1 // indirect
+ github.com/minio/md5-simd v1.1.2 // indirect
+ github.com/philhofer/fwd v1.2.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rs/xid v1.6.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
+ github.com/tetratelabs/wazero v1.9.0 // indirect
+ github.com/tinylib/msgp v1.4.0 // indirect
+ golang.org/x/crypto v0.43.0 // indirect
+ golang.org/x/image v0.27.0 // indirect
+ golang.org/x/net v0.46.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
+ google.golang.org/grpc v1.76.0 // indirect
+ google.golang.org/protobuf v1.36.10 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gofiber/storage/testhelpers/redis => github.com/gofiber/storage/redis/v3 v3.0.0
diff --git a/go.sum b/go.sum
index 17f8ff9..391f7e8 100644
--- a/go.sum
+++ b/go.sum
@@ -56,6 +56,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet v1.0.0 h1:TSp1RRgK8kP2zeisJ6m5w/HKx0z8Olw6Scw/hA6B0To=
+gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet v1.0.0/go.mod h1:B8UYYJwP8CyYUu0jleNJE610XDrCXTbcZZDxaPJ2sLE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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=
@@ -103,6 +105,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
+github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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=
@@ -119,10 +125,14 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/gen2brain/heic v0.4.5 h1:Cq3hPu6wwlTJNv2t48ro3oWje54h82Q5pALeCBNgaSk=
+github.com/gen2brain/heic v0.4.5/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -136,6 +146,8 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -210,6 +222,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -248,6 +262,12 @@ github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
+github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
+github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
+github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I=
+github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
+github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8=
+github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw=
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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@@ -264,15 +284,24 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
+github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -290,11 +319,21 @@ 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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
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/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
+github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
+github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
+github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -311,10 +350,15 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
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/pdfcpu/pdfcpu v0.11.0 h1:mL18Y3hSHzSezmnrzA21TqlayBOXuAx7BUzzZyroLGM=
+github.com/pdfcpu/pdfcpu v0.11.0/go.mod h1:F1ca4GIVFdPtmgvIdvXAycAm88noyNxZwzr9CpTy+Mw=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
+github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
+github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -340,9 +384,15 @@ 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
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=
@@ -370,6 +420,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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
+github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
+github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
+github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
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=
@@ -401,6 +457,12 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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=
@@ -413,6 +475,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
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/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
+golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -483,6 +547,11 @@ 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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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=
@@ -593,6 +662,11 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@@ -604,6 +678,11 @@ 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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
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=
@@ -791,7 +870,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-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
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=
@@ -822,6 +904,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.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
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=
@@ -837,14 +921,17 @@ 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
-gopkg.in/telebot.v4 v4.0.0-beta.4 h1:9O3elrJ1GYJhNBpi7WDlBOaM/KQPvr5xpFPUEbA+dpk=
-gopkg.in/telebot.v4 v4.0.0-beta.4/go.mod h1:jhcQjM/176jZm/s9Up/MzV5VFGPjyI8oiJhWvCMxayI=
+gopkg.in/telebot.v4 v4.0.0-beta.5 h1:uhOnORHch59vfhy09WrHLsDTwl6UIM38fiZ62jzC3dk=
+gopkg.in/telebot.v4 v4.0.0-beta.5/go.mod h1:jhcQjM/176jZm/s9Up/MzV5VFGPjyI8oiJhWvCMxayI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -852,6 +939,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/application/constants/activeStatuses.go b/internal/application/constants/activeStatuses.go
new file mode 100644
index 0000000..f2599a3
--- /dev/null
+++ b/internal/application/constants/activeStatuses.go
@@ -0,0 +1,44 @@
+package constants
+
+type ActiveStatus string
+
+const (
+ DefaultStatus ActiveStatus = "started"
+ WaitPickResumeStatus ActiveStatus = "wait-pick-resume"
+ WaitResumeStatus ActiveStatus = "wait-resume"
+ WaitWorkFormatStatus ActiveStatus = "wait-work-format"
+ WaitSalaryRangeStatus ActiveStatus = "wait-salary-range"
+ WaitTargetRoleStatus ActiveStatus = "wait-target-role"
+ WaitWorkExperienceStatus ActiveStatus = "wait-work-experience"
+ WaitQueryStatus ActiveStatus = "wait-query"
+ WaitPickJobOrMarketStatus ActiveStatus = "wait-pick-job-or-market"
+ WaitAnswerQuestionsStatus ActiveStatus = "wait-pick-answer-questions"
+ PickJobSearchStatus ActiveStatus = "pick-job-search"
+ PickMarketAnalyticStatus ActiveStatus = "pick-market-analytic"
+ FinishedStatus ActiveStatus = "finished"
+)
+
+var activeStatuses = map[ActiveStatus]struct{}{
+ DefaultStatus: {},
+ WaitPickResumeStatus: {},
+ WaitResumeStatus: {},
+ WaitWorkFormatStatus: {},
+ WaitSalaryRangeStatus: {},
+ WaitTargetRoleStatus: {},
+ WaitWorkExperienceStatus: {},
+ WaitQueryStatus: {},
+ WaitPickJobOrMarketStatus: {},
+ WaitAnswerQuestionsStatus: {},
+ PickJobSearchStatus: {},
+ PickMarketAnalyticStatus: {},
+ FinishedStatus: {},
+}
+
+func (t ActiveStatus) Valid() bool {
+ _, ok := activeStatuses[t]
+ return ok
+}
+
+func (t ActiveStatus) String() string {
+ return string(t)
+}
diff --git a/internal/application/constants/apiKey.go b/internal/application/constants/apiKey.go
new file mode 100644
index 0000000..83ae25a
--- /dev/null
+++ b/internal/application/constants/apiKey.go
@@ -0,0 +1,9 @@
+package constants
+
+type apiKey string
+
+const APIKey apiKey = "api-key"
+
+func (t apiKey) String() string {
+ return string(APIKey)
+}
diff --git a/internal/application/constants/botContent.go b/internal/application/constants/botContent.go
new file mode 100644
index 0000000..b52de30
--- /dev/null
+++ b/internal/application/constants/botContent.go
@@ -0,0 +1,53 @@
+package constants
+
+// ----------------------------------------
+// BUTTONS
+// ----------------------------------------
+const (
+ AnswerQuestionsBotButton = "Ответить на вопросы"
+ UploadResumeBotButton = "📎 Загрузить резюме"
+ JobSearchBotButton = "Хочу устроиться на работу"
+ MarketAnalyticsBotButton = "Хочу понимать что происходит на рынке"
+ SkipBotButton = "Пропустить"
+)
+
+// ----------------------------------------
+// COMMANDS
+// ----------------------------------------
+const (
+ StartBotCommand = "/start"
+ ResetBotCommand = "/reset"
+ ShowProfileBotCommand = "/profile"
+ ShowVacanciesBotCommand = "/vacancy"
+ HelpBotCommand = "/help"
+)
+
+// ----------------------------------------
+// MESSAGES
+// ----------------------------------------
+const (
+ StartBotMessage = "Привет! Я карьерный ассистент Vision Career: помогу с работой, интервью и расскажу новости по рынку, специально для тебя.\nС чего начнём?"
+ ProfileCompleteBotMessage = "Ты уже заполнил профиль. Ты можешь увидеть, что получилось в " + ShowProfileBotCommand + " или воспользоваться командой " + ResetBotCommand + " чтобы актуализировать свои данные)"
+ StartProfileCompletionBotMessage = "Окей, помогу. Скину 2–3 релевантные роли уже сегодня и мини-план подготовки, пришли свое резюме"
+ WaitResumeBotMessage = "Хорошо, жду твоё резюме в формате pdf или docx 📄"
+ UploadedResumeBotMessage = "С резюме примерно понятно, давай вернёмся к вопросам 👀"
+ SkipResumeBotMessage = "Резюме пропустим, давай вернёмся к вопросам 👀"
+ BadResumeBotMessage = "Невалидный файл..."
+ StartAnswerQuestionsBotMessage = "Уточню формат работы: "
+ BadWorkFormatBotMessage = "Невалидное значение..."
+ AcceptTargetRoleBotMessage = "Ещё нюанс: вилку по зарплате примерно в какой зоне смотреть?"
+ AcceptWorkFormatBotMessage = "Принял. Чтобы не распыляться, на какую позицию целим в первую очередь?"
+ AcceptSalaryRangeBotMessage = "Хорошо! Расскажи и своём опыте работы"
+ AcceptWorkExperienceBotMessage = "Чтобы более точно настраивать предпочтения пришли резюме."
+ FinishedBotMessage = "Отлично! Ты будешь видеть вакансии в " + ShowVacanciesBotCommand
+ NoVacanciesBotMessage = "Скоро что-нибудь подыщем для тебя!"
+ UpdateUserErrorBotMessage = "Произошла ошибка при обновлении данных("
+ InvalidWorkExperienceErrorBotMessage = "Описание опыта работы должно быть больше 100 символов"
+ SecondStartBotMessage = "Привет! Я карьерный ассистент Vision Career: помогу с работой, интервью и расскажу новости по рынку, специально для тебя.\nВоспользуйся командой " + HelpBotCommand + " чтобы ознакомиться с функционалом"
+ HelpBotMessage = "/start - изначальная команда, с помощью, которой можно начать общение со мной\n/reset - команда, с помощью, которой можно сбросить свой профиль\n/profile - команда, с помощью, которой можно посмотреть свой профиль\n/vacancy - команда, с помощью, которой можно посмотреть вакансии, которые мы нашли специально для тебя\n/help - текущая команда, чтбоы помочь тебе с функционалом"
+ SuccessResetBotMessage = "Я успешно сбросил твой профиль. Можешь начать заново, используя команду " + StartBotCommand
+ ErrorResetBotMessage = "Возникли проблемки, при сбросе твоего профиля. Пожалуйста, попробуй позже..."
+ CanNotShowProfileBotMessage = "Чтобы посмотреть профиль, необходимо полностью его заполнить..."
+ CanNotShowVacanciesBotMessage = "Чтобы посмотреть вакансии, необходимо полностью заполнить профиль("
+ GetVacanciesError = "При вычислении подходящих вакансий, возникли технические шоколадки)) Попробуй позже..."
+)
diff --git a/internal/application/constants/common.go b/internal/application/constants/common.go
index 09ab6c9..53cf0f5 100644
--- a/internal/application/constants/common.go
+++ b/internal/application/constants/common.go
@@ -2,11 +2,13 @@ package constants
const (
// Telegram
- MaxAuthTime = 300
- ChatLink = "https://t.me/"
- TelegramShare = "https://t.me/share/url?text=%s&url=%s"
- TelegramGroupPhotoURL = "https://api.telegram.org/file/bot%s/%s"
- TelegramMessageLimit = 4000
+ MaxAuthTime = 300
+ ChatLink = "https://t.me/"
+ Share = "https://t.me/share/url?text=%s&url=%s"
+ GroupPhotoURL = "https://api.telegram.org/file/bot%s/%s"
+ MessageLimit = 4000
+ SuccessEmoji = "✅"
+ ErrorEmoji = "❌"
// External resources (Support API URI`s)
// PostgreSQL
@@ -15,4 +17,6 @@ const (
DateLayout = "2006-01-02T15:04:05.000Z"
MinDate = "4713-01-01 BC"
MaxDate = "5874897-12-31"
+ // MinIO
+ ResumeBucketName = "users-resumes"
)
diff --git a/internal/application/constants/fileExtensions.go b/internal/application/constants/fileExtensions.go
new file mode 100644
index 0000000..8853af3
--- /dev/null
+++ b/internal/application/constants/fileExtensions.go
@@ -0,0 +1,24 @@
+package constants
+
+// Acceptable file extensions for import
+
+type FileExtension string
+
+const (
+ PdfFileExtension FileExtension = "pdf"
+ DocxFileExtension FileExtension = "docx"
+)
+
+var fileExtensions = map[FileExtension]struct{}{
+ PdfFileExtension: {},
+ DocxFileExtension: {},
+}
+
+func (t FileExtension) Valid() bool {
+ _, ok := fileExtensions[t]
+ return ok
+}
+
+func (t FileExtension) String() string {
+ return string(t)
+}
diff --git a/internal/application/constants/messages.go b/internal/application/constants/messages.go
deleted file mode 100644
index 9c9295e..0000000
--- a/internal/application/constants/messages.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package constants
-
-const ()
diff --git a/internal/application/constants/profile.go b/internal/application/constants/profile.go
new file mode 100644
index 0000000..3e7230b
--- /dev/null
+++ b/internal/application/constants/profile.go
@@ -0,0 +1,33 @@
+package constants
+
+type ProfileFillingStep int
+
+const (
+ MaxAge = 150
+ MinAge = 18
+ MinNameLen = 2
+ MinStringFromStepLen = 5
+ ScaleMinValue = 0
+ ScaleMaxValue = 10
+)
+
+const (
+ AboutPartnerStep ProfileFillingStep = 1
+ AboutMeStep ProfileFillingStep = 2
+ FinishStep ProfileFillingStep = 3
+)
+
+var profileFillingSteps = map[ProfileFillingStep]struct{}{
+ AboutPartnerStep: {},
+ AboutMeStep: {},
+ FinishStep: {},
+}
+
+func (t ProfileFillingStep) Valid() bool {
+ _, ok := profileFillingSteps[t]
+ return ok
+}
+
+func (t ProfileFillingStep) Int() int {
+ return int(t)
+}
diff --git a/internal/application/constants/responseStatus.go b/internal/application/constants/responseStatus.go
new file mode 100644
index 0000000..938e4c5
--- /dev/null
+++ b/internal/application/constants/responseStatus.go
@@ -0,0 +1,8 @@
+package constants
+
+type ResponseStatus string
+
+const (
+ SuccessStatus ResponseStatus = "success"
+ ErrorStatus ResponseStatus = "error"
+)
diff --git a/internal/application/constants/salaryRanges.go b/internal/application/constants/salaryRanges.go
new file mode 100644
index 0000000..4a8c2b1
--- /dev/null
+++ b/internal/application/constants/salaryRanges.go
@@ -0,0 +1,28 @@
+package constants
+
+type SalaryRange string
+
+const (
+ FirstStepSalaryRange SalaryRange = "до 180k ₽"
+ SecondStepSalaryRange SalaryRange = "180–300k ₽"
+ ThirdStepSalaryRange SalaryRange = "300–450k ₽"
+ FourthStepSalaryRange SalaryRange = "450k+ ₽"
+ DefaultSalaryRange SalaryRange = "Пока неважно"
+)
+
+var availableSalaryRanges = map[SalaryRange]struct{}{
+ FirstStepSalaryRange: {},
+ SecondStepSalaryRange: {},
+ ThirdStepSalaryRange: {},
+ FourthStepSalaryRange: {},
+ DefaultSalaryRange: {},
+}
+
+func (t SalaryRange) Valid() bool {
+ _, ok := availableSalaryRanges[t]
+ return ok
+}
+
+func (t SalaryRange) String() string {
+ return string(t)
+}
diff --git a/internal/application/constants/workFormats.go b/internal/application/constants/workFormats.go
new file mode 100644
index 0000000..07a013a
--- /dev/null
+++ b/internal/application/constants/workFormats.go
@@ -0,0 +1,26 @@
+package constants
+
+type WorkFormat string
+
+const (
+ RemoteWorkFormat WorkFormat = "Remote"
+ HybridWorkFormat WorkFormat = "Hybrid"
+ OnSiteWorkFormat WorkFormat = "On-site"
+ DefaultWorkFormat WorkFormat = "Неважно"
+)
+
+var availableWorkFormats = map[WorkFormat]struct{}{
+ RemoteWorkFormat: {},
+ HybridWorkFormat: {},
+ OnSiteWorkFormat: {},
+ DefaultWorkFormat: {},
+}
+
+func (t WorkFormat) Valid() bool {
+ _, ok := availableWorkFormats[t]
+ return ok
+}
+
+func (t WorkFormat) String() string {
+ return string(t)
+}
diff --git a/internal/application/types/models.go b/internal/application/types/models.go
index ab1254f..b53c8d5 100644
--- a/internal/application/types/models.go
+++ b/internal/application/types/models.go
@@ -1 +1,21 @@
package types
+
+import (
+ "time"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+)
+
+type User struct {
+ ID int64 `db:"id"`
+ ActiveStatus constants.ActiveStatus `db:"active_status"`
+
+ Username *string `db:"username,omitempty"`
+ TargetRole *string `db:"target_role,omitempty"`
+ ResumePath *string `db:"resume_path,omitempty"`
+ WorkExperience *string `db:"work_experience,omitempty"`
+ WorkFormat *constants.WorkFormat `db:"work_format,omitempty"`
+ SalaryRange *constants.SalaryRange `db:"salary_range,omitempty"`
+
+ CreatedAt time.Time `db:"created_at"`
+}
diff --git a/internal/application/utils/checkProfileCompletion.go b/internal/application/utils/checkProfileCompletion.go
new file mode 100644
index 0000000..527b1b7
--- /dev/null
+++ b/internal/application/utils/checkProfileCompletion.go
@@ -0,0 +1,11 @@
+package utils
+
+import "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+
+func CheckProfileCompletion(data *types.User) bool {
+ if data == nil || data.ID == 0 || data.ActiveStatus == "" || data.SalaryRange == nil ||
+ data.TargetRole == nil || data.WorkExperience == nil || data.WorkFormat == nil {
+ return false
+ }
+ return true
+}
diff --git a/internal/application/utils/fileValidations.go b/internal/application/utils/fileValidations.go
new file mode 100644
index 0000000..96c4978
--- /dev/null
+++ b/internal/application/utils/fileValidations.go
@@ -0,0 +1,143 @@
+package utils
+
+import (
+ "archive/zip"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "path/filepath"
+ "strings"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ "github.com/pdfcpu/pdfcpu/pkg/api"
+)
+
+const (
+ maxFileSize = 5 << 20 // 5 MB
+)
+
+func pdfFileValidation(fileData []byte) error {
+ if len(fileData) == 0 {
+ return errors.New("empty pdf file")
+ }
+ if len(fileData) > maxFileSize {
+ return fmt.Errorf("pdf file exceeds max size of %d bytes", maxFileSize)
+ }
+
+ // Check file signature
+ if !bytes.HasPrefix(fileData, []byte("%PDF-")) {
+ return errors.New("file does not start with %PDF- header, not a valid pdf")
+ }
+
+ // Validate PDF structure using pdfcpu (checks cross-reference tables, trailer, etc.)
+ ctx, err := api.ReadContext(bytes.NewReader(fileData), api.LoadConfiguration())
+ if err != nil {
+ return fmt.Errorf("pdf parse failed: %w", err)
+ }
+ if ctx == nil {
+ return errors.New("invalid pdf structure")
+ }
+
+ // Scan for potentially malicious content such as JavaScript or RichMedia objects
+ for _, obj := range ctx.XRefTable.Table {
+ if obj.Free {
+ continue
+ }
+ if obj.Object != nil {
+ s := fmt.Sprintf("%v", obj.Object)
+ if strings.Contains(s, "/JavaScript") || strings.Contains(s, "/JS") {
+ return errors.New("pdf contains JavaScript, potentially unsafe")
+ }
+ if strings.Contains(s, "/RichMedia") || strings.Contains(s, "/Launch") {
+ return errors.New("pdf contains embedded media or launch actions")
+ }
+ }
+ }
+
+ return nil
+}
+
+func docxFileValidation(fileData []byte) error {
+ if len(fileData) == 0 {
+ return errors.New("empty docx file")
+ }
+ if len(fileData) > maxFileSize {
+ return fmt.Errorf("docx file exceeds max size of %d bytes", maxFileSize)
+ }
+
+ // DOCX is a ZIP archive containing multiple XML files
+ reader, err := zip.NewReader(bytes.NewReader(fileData), int64(len(fileData)))
+ if err != nil {
+ return errors.New("file is not a valid DOCX (invalid zip structure)")
+ }
+
+ hasDocumentXML := false
+
+ for _, f := range reader.File {
+ name := f.Name
+
+ // Check for the main document part
+ if name == "word/document.xml" {
+ hasDocumentXML = true
+ }
+
+ // Detect and block macro files
+ if strings.EqualFold(filepath.Base(name), "vbaProject.bin") {
+ return errors.New("docx contains macros (vbaProject.bin) — potentially unsafe")
+ }
+
+ // Detect and block embedded objects
+ if strings.HasPrefix(name, "word/embeddings/") {
+ return errors.New("docx contains embedded objects — potentially unsafe")
+ }
+
+ // Verify file part size (safety check)
+ if f.UncompressedSize64 > 0 && f.UncompressedSize64 > uint64(maxFileSize) {
+ return fmt.Errorf("docx part %s is too large", name)
+ }
+
+ // Only inspect XML parts
+ if strings.HasSuffix(name, ".xml") {
+ rc, err := f.Open()
+ if err != nil {
+ return fmt.Errorf("failed to open %s: %w", name, err)
+ }
+ data, err := io.ReadAll(io.LimitReader(rc, 8192)) // read first 8KB for validation
+ rc.Close()
+ if err != nil {
+ return fmt.Errorf("failed to read %s: %w", name, err)
+ }
+
+ // Ensure XML files actually start with '<'
+ if len(data) > 0 && !bytes.HasPrefix(bytes.TrimSpace(data), []byte("<")) {
+ return fmt.Errorf("file %s inside docx is not valid XML", name)
+ }
+ }
+ }
+
+ if !hasDocumentXML {
+ return errors.New("missing main document.xml part in DOCX archive")
+ }
+
+ return nil
+}
+
+func FileValidation(fileName string, fileData []byte) error {
+ fileExtension := constants.FileExtension(strings.ReplaceAll(
+ strings.ToLower(filepath.Ext(fileName)),
+ ".", "",
+ ))
+ if !fileExtension.Valid() {
+ return errors.New("invalid extension in file name")
+ }
+
+ switch fileExtension {
+ case constants.PdfFileExtension:
+ return pdfFileValidation(fileData)
+ case constants.DocxFileExtension:
+ return docxFileValidation(fileData)
+ default:
+ return errors.New("unsupported file extension")
+ }
+}
diff --git a/internal/application/utils/validateWorkExperience.go b/internal/application/utils/validateWorkExperience.go
new file mode 100644
index 0000000..01857f7
--- /dev/null
+++ b/internal/application/utils/validateWorkExperience.go
@@ -0,0 +1,5 @@
+package utils
+
+func ValidateWorkExperience(val *string) bool {
+ return val == nil || len(*val) > 100
+}
diff --git a/internal/config/entities.go b/internal/config/entities.go
index 242808a..36dc4f8 100644
--- a/internal/config/entities.go
+++ b/internal/config/entities.go
@@ -22,6 +22,13 @@ type Postgre struct {
SSLmode bool `env:"POSTGRE_SSLMODE"`
}
+type Minio struct {
+ Endpoint string `env:"MINIO_ENDPOINT,required"`
+ User string `env:"MINIO_USER,required"`
+ Password string `env:"MINIO_PASSWORD,required"`
+ SSLmode bool `env:"MINIO_SSLMODE"`
+}
+
type Tokens struct {
MyApiKey string `env:"TOKENS_MY_API_KEY,required"`
SupportApiKey string `env:"TOKENS_SUPPORT_API_KEY,required"`
@@ -31,6 +38,7 @@ type config struct {
App *App
Bot *Bot
Postgre *Postgre
+ Minio *Minio
Tokens *Tokens
Integrations *Integrations
}
diff --git a/internal/config/loadConfig.go b/internal/config/loadConfig.go
index d1d61ea..0614693 100644
--- a/internal/config/loadConfig.go
+++ b/internal/config/loadConfig.go
@@ -27,6 +27,12 @@ func Load() error {
return err
}
Config.Postgre = postgre
+ // MinIO configuration
+ minio := new(Minio)
+ if err := env.Parse(minio); err != nil {
+ return err
+ }
+ Config.Minio = minio
// Tokens configuration
tokens := new(Tokens)
if err := env.Parse(tokens); err != nil {
diff --git a/internal/domains/profile/repository/createUser.go b/internal/domains/profile/repository/createUser.go
new file mode 100644
index 0000000..417f26f
--- /dev/null
+++ b/internal/domains/profile/repository/createUser.go
@@ -0,0 +1,36 @@
+package profileRepository
+
+import (
+ "context"
+ "time"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+func (t *repository) CreateUser(id int64, username *string) (*types.User, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ query := `
+ INSERT INTO users (id, username)
+ VALUES (:id, :username)
+ RETURNING *
+ `
+ data := &types.User{
+ ID: id,
+ Username: username,
+ }
+
+ stmt, err := t.pgDB.PrepareNamedContext(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+ defer stmt.Close()
+
+ err = stmt.GetContext(ctx, data, data)
+ if err != nil {
+ return nil, err
+ }
+
+ return data, nil
+}
diff --git a/internal/domains/profile/repository/entities.go b/internal/domains/profile/repository/entities.go
new file mode 100644
index 0000000..248b12c
--- /dev/null
+++ b/internal/domains/profile/repository/entities.go
@@ -0,0 +1,20 @@
+package profileRepository
+
+import (
+ "context"
+ "database/sql"
+
+ "github.com/jmoiron/sqlx"
+)
+
+// ----------------------------------------
+// COMMON
+// ----------------------------------------
+
+type pgDBInstance interface {
+ GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
+ PrepareNamedContext(ctx context.Context, query string) (*sqlx.NamedStmt, error)
+ ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
+ QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
+ NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error)
+}
diff --git a/internal/domains/profile/repository/getUserById.go b/internal/domains/profile/repository/getUserById.go
new file mode 100644
index 0000000..715a4d9
--- /dev/null
+++ b/internal/domains/profile/repository/getUserById.go
@@ -0,0 +1,29 @@
+package profileRepository
+
+import (
+ "context"
+ "database/sql"
+ "time"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+func (t *repository) GetUserById(id int64) (*types.User, error) {
+ data := new(types.User)
+
+ query := `
+ SELECT * FROM users
+ WHERE id = $1
+ `
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ errGetContext := t.pgDB.GetContext(ctx, data, query, id)
+ if errGetContext == sql.ErrNoRows {
+ return nil, nil
+ } else if errGetContext != nil {
+ return nil, errGetContext
+ }
+
+ return data, nil
+}
diff --git a/internal/domains/profile/repository/repository.go b/internal/domains/profile/repository/repository.go
new file mode 100644
index 0000000..ee9344c
--- /dev/null
+++ b/internal/domains/profile/repository/repository.go
@@ -0,0 +1,11 @@
+package profileRepository
+
+type repository struct {
+ pgDB pgDBInstance
+}
+
+func New(pgDBInstance pgDBInstance) *repository {
+ return &repository{
+ pgDB: pgDBInstance,
+ }
+}
diff --git a/internal/domains/profile/repository/updateUser.go b/internal/domains/profile/repository/updateUser.go
new file mode 100644
index 0000000..b55404d
--- /dev/null
+++ b/internal/domains/profile/repository/updateUser.go
@@ -0,0 +1,25 @@
+package profileRepository
+
+import (
+ "context"
+ "time"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+func (t *repository) UpdateUser(user *types.User) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ query := `
+ UPDATE users
+ SET
+ username = :username, target_role = :target_role,
+ resume_path = :resume_path, work_experience = :work_experience,
+ work_format = :work_format, salary_range = :salary_range, active_status = :active_status
+ WHERE id = :id
+ `
+
+ _, err := t.pgDB.NamedExecContext(ctx, query, user)
+ return err
+}
diff --git a/internal/domains/profile/service/checkUser.go b/internal/domains/profile/service/checkUser.go
new file mode 100644
index 0000000..8b52261
--- /dev/null
+++ b/internal/domains/profile/service/checkUser.go
@@ -0,0 +1,23 @@
+package profileService
+
+import (
+ "errors"
+ "fmt"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+func (t *service) CheckUser(id int64, username *string) (*types.User, error) {
+ op := "profileService/CheckUsers"
+ // Check if there is a user with this tg_id
+ existUser, err := t.profileRepository.GetUserById(id)
+ if existUser == nil && err == nil {
+ existUser, err = t.profileRepository.CreateUser(id, username)
+ }
+
+ if err != nil || existUser == nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return nil, errors.New("an error occurred while checking if user with this telegram id exist")
+ }
+ return existUser, nil
+}
diff --git a/internal/domains/profile/service/deleteResume.go b/internal/domains/profile/service/deleteResume.go
new file mode 100644
index 0000000..62979ac
--- /dev/null
+++ b/internal/domains/profile/service/deleteResume.go
@@ -0,0 +1,20 @@
+package profileService
+
+import (
+ "fmt"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+)
+
+func (t *service) DeleteResume(fileName string) error {
+ op := "profileService/DeleteResume"
+ // Remove resume file
+ err := t.minioDB.Remove(fileName, constants.ResumeBucketName)
+
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return fmt.Errorf("Произошла ошибка при удалени резюме...")
+ }
+
+ return nil
+}
diff --git a/internal/domains/profile/service/entities.go b/internal/domains/profile/service/entities.go
new file mode 100644
index 0000000..e5dae5f
--- /dev/null
+++ b/internal/domains/profile/service/entities.go
@@ -0,0 +1,45 @@
+package profileService
+
+import (
+ "context"
+ "io"
+
+ pbVC "gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet/pb/golang"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+ "github.com/minio/minio-go/v7"
+ "google.golang.org/grpc"
+)
+
+// ----------------------------------------
+// COMMON
+// ----------------------------------------
+
+type loggerInstance interface {
+ Error(string, ...any)
+ Debug(string, ...any)
+}
+
+type minioDBInstance interface {
+ Get(bucketName string, fileName string) (*minio.Object, error)
+ Remove(fileName string, bucketName string) error
+ UploadFile(
+ ctx context.Context, bucketName, objectName string,
+ reader io.Reader, size int64, opts minio.PutObjectOptions,
+ ) (string, error)
+ ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
+}
+
+type supportAPIInstance interface {
+ GetUserVacancies(ctx context.Context, in *pbVC.GetUserVacanciesRequest, opts ...grpc.CallOption) (*pbVC.GetUserVacanciesResponse, error)
+ UpsertUserData(ctx context.Context, in *pbVC.UpsertUserDataRequest, opts ...grpc.CallOption) (*pbVC.UpsertUserDataResponse, error)
+}
+
+// ----------------------------------------
+// REPOSITORIES
+// ----------------------------------------
+
+type profileRepository interface {
+ GetUserById(id int64) (*types.User, error)
+ CreateUser(id int64, username *string) (*types.User, error)
+ UpdateUser(user *types.User) error
+}
diff --git a/internal/domains/profile/service/getVacancies.go b/internal/domains/profile/service/getVacancies.go
new file mode 100644
index 0000000..7674d01
--- /dev/null
+++ b/internal/domains/profile/service/getVacancies.go
@@ -0,0 +1,27 @@
+package profileService
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ pbVC "gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet/pb/golang"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+)
+
+func (t *service) GetVacancies(id int64) ([]string, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ resp, err := t.supportAPI.GetUserVacancies(ctx, &pbVC.GetUserVacanciesRequest{
+ UserID: id,
+ })
+
+ if err != nil || resp.Status == string(constants.ErrorStatus) {
+ return nil, err
+ } else if resp.Status == string(constants.ErrorStatus) {
+ return nil, errors.New("unknown error")
+ }
+ return resp.Items, nil
+
+}
diff --git a/internal/domains/profile/service/saveResume.go b/internal/domains/profile/service/saveResume.go
new file mode 100644
index 0000000..5ab8e54
--- /dev/null
+++ b/internal/domains/profile/service/saveResume.go
@@ -0,0 +1,42 @@
+package profileService
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/utils"
+ "github.com/minio/minio-go/v7"
+)
+
+func (t *service) SaveResume(
+ user *types.User, fileName string,
+ fileReader io.Reader, fileSize int64,
+) (string, error) {
+ op := "profileService/SaveResume"
+ // Read file
+ data, err := io.ReadAll(fileReader)
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return "", fmt.Errorf("Произошла ошибка при проверке файла...")
+ }
+ // Validate file
+ if err := utils.FileValidation(fileName, data); err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return "", fmt.Errorf("Невалидный файл...")
+ }
+ // Save resume file
+ resumePath, err := t.minioDB.UploadFile(
+ context.Background(), constants.ResumeBucketName,
+ fileName, bytes.NewReader(data), fileSize, minio.PutObjectOptions{},
+ )
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return "", fmt.Errorf("Произошла ошибка при cохранении файла...")
+ }
+
+ return resumePath, nil
+}
diff --git a/internal/domains/profile/service/sendUserData.go b/internal/domains/profile/service/sendUserData.go
new file mode 100644
index 0000000..7d2427d
--- /dev/null
+++ b/internal/domains/profile/service/sendUserData.go
@@ -0,0 +1,39 @@
+package profileService
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ pbVC "gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet/pb/golang"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+func (t *service) sendUserData(data *types.User) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ userData := &pbVC.User{
+ Id: data.ID,
+ TargetRole: *data.TargetRole,
+ WorkExperience: *data.WorkExperience,
+ WorkFormat: data.WorkFormat.String(),
+ SalaryRange: data.SalaryRange.String(),
+ }
+
+ if data.ResumePath != nil {
+ userData.ResumePath = *data.ResumePath
+ }
+
+ resp, err := t.supportAPI.UpsertUserData(ctx, &pbVC.UpsertUserDataRequest{
+ Data: userData,
+ })
+
+ if err != nil || resp.Status == string(constants.ErrorStatus) {
+ return err
+ } else if resp.Status == string(constants.ErrorStatus) {
+ return errors.New("unknown error")
+ }
+ return nil
+}
diff --git a/internal/domains/profile/service/service.go b/internal/domains/profile/service/service.go
new file mode 100644
index 0000000..e66344f
--- /dev/null
+++ b/internal/domains/profile/service/service.go
@@ -0,0 +1,22 @@
+package profileService
+
+type service struct {
+ logger loggerInstance
+ minioDB minioDBInstance
+ supportAPI supportAPIInstance
+ profileRepository profileRepository
+}
+
+func New(
+ loggerInstance loggerInstance,
+ minioDBInstance minioDBInstance,
+ supportAPIInstance supportAPIInstance,
+ profileRepository profileRepository,
+) *service {
+ return &service{
+ logger: loggerInstance,
+ minioDB: minioDBInstance,
+ supportAPI: supportAPIInstance,
+ profileRepository: profileRepository,
+ }
+}
diff --git a/internal/domains/profile/service/updateUser.go b/internal/domains/profile/service/updateUser.go
new file mode 100644
index 0000000..abaf381
--- /dev/null
+++ b/internal/domains/profile/service/updateUser.go
@@ -0,0 +1,34 @@
+package profileService
+
+import (
+ "errors"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/utils"
+)
+
+func (t *service) UpdateUser(user *types.User) error {
+ if !user.ActiveStatus.Valid() {
+ return errors.New("invalid active status")
+ } else if user.SalaryRange != nil && !user.SalaryRange.Valid() {
+ return errors.New("invalid salary range")
+ } else if user.WorkFormat != nil && !user.WorkFormat.Valid() {
+ return errors.New("invalid work format")
+ } else if !utils.ValidateWorkExperience(user.WorkExperience) {
+ return errors.New("invalid work experience")
+ }
+
+ if err := t.profileRepository.UpdateUser(user); err != nil {
+ return err
+ }
+
+ if !utils.CheckProfileCompletion(user) {
+ return nil
+ }
+
+ if err := t.sendUserData(user); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/infrastructure/botService/bot.go b/internal/infrastructure/botService/bot.go
index d9df8e3..6d28024 100644
--- a/internal/infrastructure/botService/bot.go
+++ b/internal/infrastructure/botService/bot.go
@@ -9,6 +9,9 @@ import (
"time"
"gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/config"
+ profileRepository "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/domains/profile/repository"
+ profileService "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/domains/profile/service"
+ profileHandler "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/infrastructure/botService/handlers/profile"
tele "gopkg.in/telebot.v4"
)
@@ -16,6 +19,7 @@ func Start(
botCfg *config.Bot,
loggerInstance loggerInstance,
pgDBInstance pgDBInstance,
+ minioDBInstance minioDBInstance,
supportAPIInstance supportAPIInstance,
) error {
// Create bot instance with optimized settings
@@ -64,14 +68,14 @@ func Start(
if err != nil {
return err
}
- // Add global middlewares
-
// Create repositories
-
+ profileRepository := profileRepository.New(pgDBInstance)
// Create services
-
+ profileService := profileService.New(loggerInstance, minioDBInstance, supportAPIInstance, profileRepository)
// Create handlers
-
+ profileHandler := profileHandler.New(bot, loggerInstance, profileService)
+ // Init handlers
+ profileHandler.Init()
// Start cron jobs
// Create channels for graceful shutdown
diff --git a/internal/infrastructure/botService/entities.go b/internal/infrastructure/botService/entities.go
index 6b6d952..74d6a35 100644
--- a/internal/infrastructure/botService/entities.go
+++ b/internal/infrastructure/botService/entities.go
@@ -4,9 +4,13 @@ import (
"context"
"database/sql"
"database/sql/driver"
+ "io"
"time"
+ pbVC "gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet/pb/golang"
"github.com/jmoiron/sqlx"
+ "github.com/minio/minio-go/v7"
+ "google.golang.org/grpc"
)
// ----------------------------------------
@@ -70,11 +74,17 @@ type pgDBInstance interface {
Unsafe() *sqlx.DB
}
-type supportAPIInstance interface {
- DefaultRequest(
- ctx context.Context, acceptStatus int,
- uri, httpMethod string,
- body, result interface{},
- queryParams map[string]string,
- ) (*int, error)
+type minioDBInstance interface {
+ Get(bucketName string, fileName string) (*minio.Object, error)
+ Remove(fileName string, bucketName string) error
+ UploadFile(
+ ctx context.Context, bucketName, objectName string,
+ reader io.Reader, size int64, opts minio.PutObjectOptions,
+ ) (string, error)
+ ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
+}
+
+type supportAPIInstance interface {
+ GetUserVacancies(ctx context.Context, in *pbVC.GetUserVacanciesRequest, opts ...grpc.CallOption) (*pbVC.GetUserVacanciesResponse, error)
+ UpsertUserData(ctx context.Context, in *pbVC.UpsertUserDataRequest, opts ...grpc.CallOption) (*pbVC.UpsertUserDataResponse, error)
}
diff --git a/internal/infrastructure/botService/handlers/profile/entities.go b/internal/infrastructure/botService/handlers/profile/entities.go
new file mode 100644
index 0000000..48347a7
--- /dev/null
+++ b/internal/infrastructure/botService/handlers/profile/entities.go
@@ -0,0 +1,30 @@
+package profileHandler
+
+import (
+ "io"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+)
+
+// ----------------------------------------
+// COMMON
+// ----------------------------------------
+
+type loggerInstance interface {
+ Error(string, ...any)
+}
+
+// ----------------------------------------
+// SERVICES
+// ----------------------------------------
+
+type profileService interface {
+ CheckUser(id int64, username *string) (*types.User, error)
+ UpdateUser(user *types.User) error
+ SaveResume(
+ user *types.User, fileName string,
+ fileReader io.Reader, fileSize int64,
+ ) (string, error)
+ DeleteResume(fileName string) error
+ GetVacancies(id int64) ([]string, error)
+}
diff --git a/internal/infrastructure/botService/handlers/profile/handler.go b/internal/infrastructure/botService/handlers/profile/handler.go
new file mode 100644
index 0000000..f4bc330
--- /dev/null
+++ b/internal/infrastructure/botService/handlers/profile/handler.go
@@ -0,0 +1,28 @@
+package profileHandler
+
+import (
+ tele "gopkg.in/telebot.v4"
+)
+
+type handler struct {
+ bot *tele.Bot
+ logger loggerInstance
+ profileService profileService
+}
+
+func (t *handler) Init() {
+ t.bot.Handle(tele.OnText, t.universalTextHandler)
+ t.bot.Handle(tele.OnDocument, t.universalDocumentHandler)
+}
+
+func New(
+ bot *tele.Bot,
+ loggerInstance loggerInstance,
+ profileService profileService,
+) *handler {
+ return &handler{
+ bot: bot,
+ logger: loggerInstance,
+ profileService: profileService,
+ }
+}
diff --git a/internal/infrastructure/botService/handlers/profile/universalDocumentHandler.go b/internal/infrastructure/botService/handlers/profile/universalDocumentHandler.go
new file mode 100644
index 0000000..9fe725b
--- /dev/null
+++ b/internal/infrastructure/botService/handlers/profile/universalDocumentHandler.go
@@ -0,0 +1,80 @@
+package profileHandler
+
+import (
+ "fmt"
+ "runtime/debug"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ tele "gopkg.in/telebot.v4"
+)
+
+func (t *handler) universalDocumentHandler(ctx tele.Context) error {
+ op := "profileHandler/universalDocumentHandler"
+ // Start recovering
+ defer func() {
+ if r := recover(); r != nil {
+ t.logger.Error(fmt.Sprintf("%v - Panic recovered: %v\nStack trace:\n%s", op, r, debug.Stack()))
+ }
+ }()
+ // Common variables
+ msg := ""
+ val := ctx.Message().Document
+ needUpdateUser := true
+ mainKeys := new(tele.ReplyMarkup)
+ // Check user
+ var id int64
+ var username *string
+
+ if ctx.Chat().Username == "" {
+ username = nil
+ } else {
+ username = &ctx.Chat().Username
+ }
+ id = ctx.Chat().ID
+
+ user, err := t.profileService.CheckUser(id, username)
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return nil
+ }
+ // Check document and fetch file
+ if val == nil {
+ return ctx.Send(constants.BadResumeBotMessage)
+ } else {
+ reader, err := t.bot.File(&val.File)
+ if err != nil {
+ return fmt.Errorf("failed to fetch file: %w", err)
+ }
+ defer reader.Close()
+ val.File.FileReader = reader
+ }
+ // Check active status and save current value
+ if user.ActiveStatus == constants.WaitResumeStatus {
+ // Save file
+ resumePath, err := t.profileService.SaveResume(user, val.File.FilePath, val.File.FileReader, val.File.FileSize)
+ if err != nil {
+ return ctx.Send(err.Error())
+ }
+ // Save resume file path and set new active stage
+ user.ResumePath = &resumePath
+ user.ActiveStatus = constants.WaitAnswerQuestionsStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ answerQuestionsBtn := mainKeys.Text(constants.AnswerQuestionsBotButton)
+ mainKeys.Reply(
+ mainKeys.Row(answerQuestionsBtn),
+ )
+
+ msg = constants.UploadedResumeBotMessage
+ }
+ // Set new user data
+ if needUpdateUser {
+ err = t.profileService.UpdateUser(user)
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return ctx.Send(constants.UpdateUserErrorBotMessage)
+ }
+ }
+
+ return ctx.Send(msg, mainKeys)
+}
diff --git a/internal/infrastructure/botService/handlers/profile/universalTextHandler.go b/internal/infrastructure/botService/handlers/profile/universalTextHandler.go
new file mode 100644
index 0000000..3c86ee7
--- /dev/null
+++ b/internal/infrastructure/botService/handlers/profile/universalTextHandler.go
@@ -0,0 +1,229 @@
+package profileHandler
+
+import (
+ "fmt"
+ "runtime/debug"
+ "strings"
+
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/types"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/utils"
+ tele "gopkg.in/telebot.v4"
+)
+
+func (t *handler) universalTextHandler(ctx tele.Context) error {
+ op := "profileHandler/universalTextHandler"
+ // Start recovering
+ defer func() {
+ if r := recover(); r != nil {
+ t.logger.Error(fmt.Sprintf("%v - Panic recovered: %v\nStack trace:\n%s", op, r, debug.Stack()))
+ }
+ }()
+ // Common variables
+ msg := ""
+ val := ctx.Text()
+ needUpdateUser := true
+ mainKeys := new(tele.ReplyMarkup)
+ // Check user
+ var id int64
+ var username *string
+
+ if ctx.Chat().Username == "" {
+ username = nil
+ } else {
+ username = &ctx.Chat().Username
+ }
+ id = ctx.Chat().ID
+
+ user, err := t.profileService.CheckUser(id, username)
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return nil
+ }
+ // Check active status and save current value
+ if val == constants.ResetBotCommand {
+ if user.ResumePath != nil {
+ err := t.profileService.DeleteResume(*user.ResumePath)
+ if err != nil {
+ return ctx.Send(constants.ErrorResetBotMessage)
+ }
+ }
+ user = &types.User{
+ ID: user.ID,
+ Username: user.Username,
+ ActiveStatus: constants.DefaultStatus,
+ TargetRole: nil,
+ ResumePath: nil,
+ WorkExperience: nil,
+ WorkFormat: nil,
+ SalaryRange: nil,
+ }
+ msg = constants.SuccessResetBotMessage
+ } else if val == constants.HelpBotCommand {
+ needUpdateUser = false
+ msg = constants.HelpBotMessage
+ } else if (user.ActiveStatus == constants.DefaultStatus) && (val == constants.StartBotCommand) {
+ user.ActiveStatus = constants.WaitPickJobOrMarketStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ jobSearchBtn := mainKeys.Text(constants.JobSearchBotButton)
+ // TODO: marketAnalyticsBtn := mainKeys.Text(constants.MarketAnalyticsBotButton)
+ mainKeys.Reply(
+ mainKeys.Row(jobSearchBtn),
+ // TODO: mainKeys.Row(marketAnalyticsBtn),
+ )
+
+ msg = constants.StartBotMessage
+ } else if (user.ActiveStatus != constants.DefaultStatus) && (val == constants.StartBotCommand) {
+ needUpdateUser = false
+ msg = constants.SecondStartBotMessage
+ } else if (user.ActiveStatus == constants.WaitPickJobOrMarketStatus) && (val == constants.JobSearchBotButton) {
+ user.ActiveStatus = constants.WaitPickResumeStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ uploadResumeBtn := mainKeys.Text(constants.UploadResumeBotButton)
+ skipBtn := mainKeys.Text(constants.SkipBotButton)
+ mainKeys.Reply(
+ mainKeys.Row(uploadResumeBtn),
+ mainKeys.Row(skipBtn),
+ )
+
+ msg = constants.StartProfileCompletionBotMessage
+ } else if (user.ActiveStatus == constants.WaitPickResumeStatus) && (val == constants.SkipBotButton) {
+ user.ActiveStatus = constants.WaitAnswerQuestionsStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ answerQuestionsBtn := mainKeys.Text(constants.AnswerQuestionsBotButton)
+ mainKeys.Reply(
+ mainKeys.Row(answerQuestionsBtn),
+ )
+
+ msg = constants.SkipResumeBotMessage
+ } else if (user.ActiveStatus == constants.WaitPickResumeStatus) && (val == constants.UploadResumeBotButton) {
+ user.ActiveStatus = constants.WaitResumeStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.WaitResumeBotMessage
+ } else if (user.ActiveStatus == constants.WaitResumeStatus) && (val == constants.UploadResumeBotButton) {
+ needUpdateUser = false
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.WaitResumeBotMessage
+ } else if (user.ActiveStatus == constants.WaitAnswerQuestionsStatus) && (val == constants.AnswerQuestionsBotButton) {
+ user.ActiveStatus = constants.WaitWorkFormatStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ remoteBtn := mainKeys.Text(constants.RemoteWorkFormat.String())
+ hybridBtn := mainKeys.Text(constants.HybridWorkFormat.String())
+ onSiteBtn := mainKeys.Text(constants.OnSiteWorkFormat.String())
+ defaultBtn := mainKeys.Text(constants.DefaultWorkFormat.String())
+ mainKeys.Reply(
+ mainKeys.Row(remoteBtn),
+ mainKeys.Row(hybridBtn),
+ mainKeys.Row(onSiteBtn),
+ mainKeys.Row(defaultBtn),
+ )
+
+ msg = constants.StartAnswerQuestionsBotMessage
+ } else if user.ActiveStatus == constants.WaitWorkFormatStatus {
+ wf := constants.WorkFormat(val)
+ user.WorkFormat = &wf
+ user.ActiveStatus = constants.WaitTargetRoleStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.AcceptWorkFormatBotMessage
+ } else if user.ActiveStatus == constants.WaitTargetRoleStatus {
+ user.TargetRole = &val
+ user.ActiveStatus = constants.WaitSalaryRangeStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{ResizeKeyboard: true}
+ firstStepBtn := mainKeys.Text(constants.FirstStepSalaryRange.String())
+ secondStepBtn := mainKeys.Text(constants.SecondStepSalaryRange.String())
+ thirdStepBtn := mainKeys.Text(constants.ThirdStepSalaryRange.String())
+ fourthStepBtn := mainKeys.Text(constants.FourthStepSalaryRange.String())
+ defaultStepBtn := mainKeys.Text(constants.DefaultSalaryRange.String())
+ mainKeys.Reply(
+ mainKeys.Row(firstStepBtn),
+ mainKeys.Row(secondStepBtn),
+ mainKeys.Row(thirdStepBtn),
+ mainKeys.Row(fourthStepBtn),
+ mainKeys.Row(defaultStepBtn),
+ )
+
+ msg = constants.AcceptTargetRoleBotMessage
+ } else if user.ActiveStatus == constants.WaitSalaryRangeStatus {
+ sr := constants.SalaryRange(val)
+ user.SalaryRange = &sr
+ user.ActiveStatus = constants.WaitWorkExperienceStatus
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.AcceptSalaryRangeBotMessage
+ } else if user.ActiveStatus == constants.WaitWorkExperienceStatus {
+ user.WorkExperience = &val
+ user.ActiveStatus = constants.FinishedStatus
+
+ if !utils.ValidateWorkExperience(&val) {
+ return ctx.Send(constants.InvalidWorkExperienceErrorBotMessage)
+ }
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.FinishedBotMessage
+ } else if val == constants.ShowProfileBotCommand && !utils.CheckProfileCompletion(user) {
+ needUpdateUser = false
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.CanNotShowProfileBotMessage
+ } else if val == constants.ShowProfileBotCommand && utils.CheckProfileCompletion(user) {
+ needUpdateUser = false
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+ var resume string
+
+ if user.ResumePath == nil {
+ resume = constants.ErrorEmoji
+ } else {
+ resume = constants.SuccessEmoji
+ }
+
+ msg = fmt.Sprintf(
+ "Резюме: %v\nЗарплатная вилка: %v\nДолжность: %v\nОпыт работы: %v\nФормат работы: %v\n",
+ resume, user.SalaryRange, *user.TargetRole, *user.WorkExperience, user.WorkFormat,
+ )
+ } else if val == constants.ShowVacanciesBotCommand && !utils.CheckProfileCompletion(user) {
+ needUpdateUser = false
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = constants.CanNotShowVacanciesBotMessage
+ } else if val == constants.ShowVacanciesBotCommand && utils.CheckProfileCompletion(user) {
+ vacancies, err := t.profileService.GetVacancies(user.ID)
+
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return ctx.Send(constants.GetVacanciesError)
+ } else if len(vacancies) == 0 {
+ return ctx.Send(constants.NoVacanciesBotMessage)
+ }
+ needUpdateUser = false
+ // Set menu
+ mainKeys = &tele.ReplyMarkup{RemoveKeyboard: true}
+
+ msg = strings.Join(vacancies, "\n")
+ }
+ // Set new user data
+ if needUpdateUser {
+ err = t.profileService.UpdateUser(user)
+ if err != nil {
+ t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
+ return ctx.Send(constants.UpdateUserErrorBotMessage)
+ }
+ }
+
+ return ctx.Send(msg, mainKeys)
+}
diff --git a/internal/infrastructure/grpcClient/client.go b/internal/infrastructure/grpcClient/client.go
new file mode 100644
index 0000000..d00881d
--- /dev/null
+++ b/internal/infrastructure/grpcClient/client.go
@@ -0,0 +1,57 @@
+package grpcClient
+
+import (
+ "context"
+
+ pbVC "gitea.cybertalant.ru/VisionCareerMiniapp/DataManagemet/pb/golang"
+ "gitea.cybertalant.ru/VisionCareerMiniapp/MiniappGoService/internal/application/constants"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+)
+
+type grpcClient struct {
+ apiKey string
+ conn *grpc.ClientConn
+ vсClient pbVC.VisionCareerApiServiceV1Client
+}
+
+func (t *grpcClient) Close() error {
+ return t.conn.Close()
+}
+
+func (t *grpcClient) GetUserVacancies(ctx context.Context, in *pbVC.GetUserVacanciesRequest, opts ...grpc.CallOption) (*pbVC.GetUserVacanciesResponse, error) {
+ // Append api key to Meta-data
+ md := metadata.Pairs(constants.APIKey.String(), t.apiKey)
+ ctx = metadata.NewOutgoingContext(ctx, md)
+
+ return t.vсClient.GetUserVacancies(ctx, in, opts...)
+}
+
+func (t *grpcClient) UpsertUserData(ctx context.Context, in *pbVC.UpsertUserDataRequest, opts ...grpc.CallOption) (*pbVC.UpsertUserDataResponse, error) {
+ // Append api key to Meta-data
+ md := metadata.Pairs(constants.APIKey.String(), t.apiKey)
+ ctx = metadata.NewOutgoingContext(ctx, md)
+
+ return t.vсClient.UpsertUserData(ctx, in, opts...)
+}
+
+// Init : Инициализирует общий gRPC клиент
+func Init(addr, apiKey string) (*grpcClient, error) {
+ // Открытие соединения
+ conn, err := grpc.Dial(
+ addr,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithBlock(),
+ )
+
+ if err != nil {
+ return nil, err
+ }
+ // Инициализация gRPC клиентов
+ return &grpcClient{
+ apiKey: apiKey,
+ conn: conn,
+ vсClient: pbVC.NewVisionCareerApiServiceV1Client(conn),
+ }, nil
+}
diff --git a/internal/infrastructure/httpClient/client.go b/internal/infrastructure/httpClient/client.go
deleted file mode 100644
index 4ce7670..0000000
--- a/internal/infrastructure/httpClient/client.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package httpClient
-
-import (
- "net/http"
- "time"
-)
-
-type HttpClient struct {
- client *http.Client
- endpoint string
- token string
- logger loggerInstance
-}
-
-func (t *HttpClient) Close() error {
- if transport, ok := t.client.Transport.(*http.Transport); ok {
- transport.CloseIdleConnections()
- }
- return nil
-}
-
-func Init(
- loggerInstance loggerInstance,
- endpoint, token string,
-) *HttpClient {
- return &HttpClient{
- logger: loggerInstance,
-
- endpoint: endpoint,
- token: token,
-
- client: &http.Client{
- Timeout: 50 * time.Second,
- Transport: &http.Transport{
- MaxIdleConns: 20,
- IdleConnTimeout: 190 * time.Second,
- },
- },
- }
-}
diff --git a/internal/infrastructure/httpClient/defaultRequest.go b/internal/infrastructure/httpClient/defaultRequest.go
deleted file mode 100644
index ba85101..0000000
--- a/internal/infrastructure/httpClient/defaultRequest.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package httpClient
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-func (t *HttpClient) DefaultRequest(
- ctx context.Context, acceptStatus int,
- uri, httpMethod string,
- body, result interface{},
- queryParams map[string]string,
-) (*int, error) {
- op := "httpClient/DefaultRequest"
-
- var req *http.Request
- var err error
- url := fmt.Sprintf("%v%v", t.endpoint, uri)
-
- if body != nil {
- bodyBytes, err := json.Marshal(body)
- if err != nil {
- t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
- return nil, fmt.Errorf("failed to marshal request body")
- }
- req, err = http.NewRequest(httpMethod, url, bytes.NewReader(bodyBytes))
- } else {
- req, err = http.NewRequest(httpMethod, url, nil)
- }
- t.logger.Info(fmt.Sprintf("Sending a request %v %v", httpMethod, url))
-
- if err != nil {
- t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
- return nil, fmt.Errorf("failed to create request")
- }
-
- req.Header.Set("Authorization", "Bearer "+t.token)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Accept", "application/json")
- if queryParams != nil {
- q := req.URL.Query()
- for key, value := range queryParams {
- q.Add(key, value)
- }
- req.URL.RawQuery = q.Encode()
- }
-
- resp, err := t.client.Do(req)
- if err != nil || resp == nil {
- t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
- return nil, fmt.Errorf("failed to perform request")
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != acceptStatus {
- var errMsg interface{}
- json.NewDecoder(resp.Body).Decode(&errMsg)
- t.logger.Error(fmt.Sprintf("%v: %v", op, errMsg))
- return &resp.StatusCode, fmt.Errorf("an error occurred while processing the response")
- }
-
- if result == nil {
- return &resp.StatusCode, nil
- }
-
- if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
- t.logger.Error(fmt.Sprintf("%v: %v", op, err.Error()))
- return &resp.StatusCode, fmt.Errorf("failed to decode response")
- }
- return &resp.StatusCode, nil
-}
diff --git a/internal/infrastructure/httpClient/entities.go b/internal/infrastructure/httpClient/entities.go
deleted file mode 100644
index 75b2d72..0000000
--- a/internal/infrastructure/httpClient/entities.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package httpClient
-
-// ----------------------------------------
-// COMMON
-// ----------------------------------------
-
-type loggerInstance interface {
- Info(string, ...any)
- Error(string, ...any)
-}
diff --git a/migrations/000001_create_users_table.down.sql b/migrations/000001_create_users_table.down.sql
new file mode 100644
index 0000000..365a210
--- /dev/null
+++ b/migrations/000001_create_users_table.down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS users;
\ No newline at end of file
diff --git a/migrations/000001_create_users_table.up.sql b/migrations/000001_create_users_table.up.sql
new file mode 100644
index 0000000..6ef6753
--- /dev/null
+++ b/migrations/000001_create_users_table.up.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS users (
+ id BIGINT PRIMARY KEY,
+ username TEXT UNIQUE,
+ target_role TEXT,
+ resume_path TEXT,
+ work_experience TEXT,
+ work_format TEXT,
+ salary_range TEXT,
+ active_status TEXT NOT NULL DEFAULT 'started',
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
diff --git a/pkg/minioDB/createBucket.go b/pkg/minioDB/createBucket.go
new file mode 100644
index 0000000..d05ad5b
--- /dev/null
+++ b/pkg/minioDB/createBucket.go
@@ -0,0 +1,11 @@
+package minioDB
+
+import (
+ "context"
+
+ "github.com/minio/minio-go/v7"
+)
+
+func (c *client) createBucket(name string) error {
+ return c.minioClient.MakeBucket(context.Background(), name, minio.MakeBucketOptions{})
+}
diff --git a/pkg/minioDB/existBucket.go b/pkg/minioDB/existBucket.go
new file mode 100644
index 0000000..b2b6fd6
--- /dev/null
+++ b/pkg/minioDB/existBucket.go
@@ -0,0 +1,12 @@
+package minioDB
+
+import "context"
+
+func (c *client) existBucket(name string) (bool, error) {
+ exist, errBucketExists := c.minioClient.BucketExists(context.Background(), name)
+
+ if errBucketExists != nil {
+ return false, errBucketExists
+ }
+ return exist, nil
+}
diff --git a/pkg/minioDB/generateFileName.go b/pkg/minioDB/generateFileName.go
new file mode 100644
index 0000000..101e1e9
--- /dev/null
+++ b/pkg/minioDB/generateFileName.go
@@ -0,0 +1,16 @@
+package minioDB
+
+import (
+ "fmt"
+
+ "github.com/google/uuid"
+)
+
+func (c *client) generateFileName(fileName string) (string, error) {
+ uuid, errNewRandom := uuid.NewRandom()
+ if errNewRandom != nil {
+ return "", errNewRandom
+ }
+
+ return fmt.Sprintf("%s_%s", uuid.String(), fileName), nil
+}
diff --git a/pkg/minioDB/get.go b/pkg/minioDB/get.go
new file mode 100644
index 0000000..b0aea1f
--- /dev/null
+++ b/pkg/minioDB/get.go
@@ -0,0 +1,13 @@
+package minioDB
+
+import (
+ "context"
+
+ "github.com/minio/minio-go/v7"
+)
+
+func (c *client) Get(
+ bucketName, fileName string,
+) (*minio.Object, error) {
+ return c.minioClient.GetObject(context.Background(), bucketName, fileName, minio.GetObjectOptions{})
+}
diff --git a/pkg/minioDB/listBuckets.go b/pkg/minioDB/listBuckets.go
new file mode 100644
index 0000000..b013240
--- /dev/null
+++ b/pkg/minioDB/listBuckets.go
@@ -0,0 +1,11 @@
+package minioDB
+
+import (
+ "context"
+
+ "github.com/minio/minio-go/v7"
+)
+
+func (t *client) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
+ return t.minioClient.ListBuckets(ctx)
+}
diff --git a/pkg/minioDB/minio.go b/pkg/minioDB/minio.go
new file mode 100644
index 0000000..1847db3
--- /dev/null
+++ b/pkg/minioDB/minio.go
@@ -0,0 +1,36 @@
+package minioDB
+
+import (
+ "crypto/tls"
+ "net/http"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+)
+
+type client struct {
+ minioClient *minio.Client
+}
+
+func New(
+ endpoint, accessKeyID, secretAccessKey string,
+ useSSL bool,
+) (*client, error) {
+ minioClient, errNew := minio.New(endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
+ Secure: useSSL,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ })
+ if errNew != nil {
+ return nil, errNew
+ }
+
+ client := &client{
+ minioClient: minioClient,
+ }
+ return client, nil
+}
diff --git a/pkg/minioDB/remove.go b/pkg/minioDB/remove.go
new file mode 100644
index 0000000..8bd97cb
--- /dev/null
+++ b/pkg/minioDB/remove.go
@@ -0,0 +1,13 @@
+package minioDB
+
+import (
+ "context"
+
+ "github.com/minio/minio-go/v7"
+)
+
+func (c *client) Remove(
+ fileName, bucketName string,
+) error {
+ return c.minioClient.RemoveObject(context.Background(), bucketName, fileName, minio.RemoveObjectOptions{})
+}
diff --git a/pkg/minioDB/uploadFile.go b/pkg/minioDB/uploadFile.go
new file mode 100644
index 0000000..8fa3dcd
--- /dev/null
+++ b/pkg/minioDB/uploadFile.go
@@ -0,0 +1,48 @@
+package minioDB
+
+import (
+ "context"
+ "fmt"
+ _ "image/gif"
+ _ "image/jpeg"
+ "io"
+
+ _ "github.com/gen2brain/heic"
+ "github.com/minio/minio-go/v7"
+)
+
+// UploadFile uploads an image to MinIO
+func (c *client) UploadFile(
+ ctx context.Context, bucketName, objectName string,
+ reader io.Reader, size int64, opts minio.PutObjectOptions,
+) (string, error) {
+ // Ensure bucket exists
+ exist, err := c.existBucket(bucketName)
+ if err != nil {
+ return "", fmt.Errorf("failed to check bucket: %v", err)
+ }
+ if !exist {
+ if err := c.createBucket(bucketName); err != nil {
+ return "", fmt.Errorf("failed to create bucket: %v", err)
+ }
+ }
+ if bucketName == "" {
+ return "", fmt.Errorf("bucket name cannot be empty")
+ }
+ // Generate secure file name
+ name, err := c.generateFileName(objectName)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate file name: %v", err)
+ }
+ // Upload to MinIO
+ _, err = c.minioClient.PutObject(
+ ctx, bucketName, name,
+ reader, size, opts,
+ )
+
+ if err != nil {
+ return "", fmt.Errorf("failed to upload file: %v", err)
+ }
+
+ return name, nil
+}