feat: Added main logic for telegram bot.
This commit is contained in:
parent
b61482bdd4
commit
bad3c8f6c6
18
cmd/main.go
18
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
36
go.mod
36
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
|
||||
|
||||
92
go.sum
92
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=
|
||||
|
||||
44
internal/application/constants/activeStatuses.go
Normal file
44
internal/application/constants/activeStatuses.go
Normal file
@ -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)
|
||||
}
|
||||
9
internal/application/constants/apiKey.go
Normal file
9
internal/application/constants/apiKey.go
Normal file
@ -0,0 +1,9 @@
|
||||
package constants
|
||||
|
||||
type apiKey string
|
||||
|
||||
const APIKey apiKey = "api-key"
|
||||
|
||||
func (t apiKey) String() string {
|
||||
return string(APIKey)
|
||||
}
|
||||
53
internal/application/constants/botContent.go
Normal file
53
internal/application/constants/botContent.go
Normal file
@ -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 = "Привет! Я карьерный ассистент <b>Vision Career</b>: помогу с работой, интервью и расскажу новости по рынку, специально для тебя.\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 = "Привет! Я карьерный ассистент <b>Vision Career</b>: помогу с работой, интервью и расскажу новости по рынку, специально для тебя.\nВоспользуйся командой " + HelpBotCommand + " чтобы ознакомиться с функционалом"
|
||||
HelpBotMessage = "<b>/start</b> - изначальная команда, с помощью, которой можно начать общение со мной\n<b>/reset</b> - команда, с помощью, которой можно сбросить свой профиль\n<b>/profile</b> - команда, с помощью, которой можно посмотреть свой профиль\n<b>/vacancy</b> - команда, с помощью, которой можно посмотреть вакансии, которые мы нашли специально для тебя\n<b>/help</b> - текущая команда, чтбоы помочь тебе с функционалом"
|
||||
SuccessResetBotMessage = "Я успешно сбросил твой профиль. Можешь начать заново, используя команду " + StartBotCommand
|
||||
ErrorResetBotMessage = "Возникли проблемки, при сбросе твоего профиля. Пожалуйста, попробуй позже..."
|
||||
CanNotShowProfileBotMessage = "Чтобы посмотреть профиль, необходимо полностью его заполнить..."
|
||||
CanNotShowVacanciesBotMessage = "Чтобы посмотреть вакансии, необходимо полностью заполнить профиль("
|
||||
GetVacanciesError = "При вычислении подходящих вакансий, возникли технические шоколадки)) Попробуй позже..."
|
||||
)
|
||||
@ -4,9 +4,11 @@ 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
|
||||
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"
|
||||
)
|
||||
|
||||
24
internal/application/constants/fileExtensions.go
Normal file
24
internal/application/constants/fileExtensions.go
Normal file
@ -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)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
package constants
|
||||
|
||||
const ()
|
||||
33
internal/application/constants/profile.go
Normal file
33
internal/application/constants/profile.go
Normal file
@ -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)
|
||||
}
|
||||
8
internal/application/constants/responseStatus.go
Normal file
8
internal/application/constants/responseStatus.go
Normal file
@ -0,0 +1,8 @@
|
||||
package constants
|
||||
|
||||
type ResponseStatus string
|
||||
|
||||
const (
|
||||
SuccessStatus ResponseStatus = "success"
|
||||
ErrorStatus ResponseStatus = "error"
|
||||
)
|
||||
28
internal/application/constants/salaryRanges.go
Normal file
28
internal/application/constants/salaryRanges.go
Normal file
@ -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)
|
||||
}
|
||||
26
internal/application/constants/workFormats.go
Normal file
26
internal/application/constants/workFormats.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
11
internal/application/utils/checkProfileCompletion.go
Normal file
11
internal/application/utils/checkProfileCompletion.go
Normal file
@ -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
|
||||
}
|
||||
143
internal/application/utils/fileValidations.go
Normal file
143
internal/application/utils/fileValidations.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
5
internal/application/utils/validateWorkExperience.go
Normal file
5
internal/application/utils/validateWorkExperience.go
Normal file
@ -0,0 +1,5 @@
|
||||
package utils
|
||||
|
||||
func ValidateWorkExperience(val *string) bool {
|
||||
return val == nil || len(*val) > 100
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
36
internal/domains/profile/repository/createUser.go
Normal file
36
internal/domains/profile/repository/createUser.go
Normal file
@ -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
|
||||
}
|
||||
20
internal/domains/profile/repository/entities.go
Normal file
20
internal/domains/profile/repository/entities.go
Normal file
@ -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)
|
||||
}
|
||||
29
internal/domains/profile/repository/getUserById.go
Normal file
29
internal/domains/profile/repository/getUserById.go
Normal file
@ -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
|
||||
}
|
||||
11
internal/domains/profile/repository/repository.go
Normal file
11
internal/domains/profile/repository/repository.go
Normal file
@ -0,0 +1,11 @@
|
||||
package profileRepository
|
||||
|
||||
type repository struct {
|
||||
pgDB pgDBInstance
|
||||
}
|
||||
|
||||
func New(pgDBInstance pgDBInstance) *repository {
|
||||
return &repository{
|
||||
pgDB: pgDBInstance,
|
||||
}
|
||||
}
|
||||
25
internal/domains/profile/repository/updateUser.go
Normal file
25
internal/domains/profile/repository/updateUser.go
Normal file
@ -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
|
||||
}
|
||||
23
internal/domains/profile/service/checkUser.go
Normal file
23
internal/domains/profile/service/checkUser.go
Normal file
@ -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
|
||||
}
|
||||
20
internal/domains/profile/service/deleteResume.go
Normal file
20
internal/domains/profile/service/deleteResume.go
Normal file
@ -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
|
||||
}
|
||||
45
internal/domains/profile/service/entities.go
Normal file
45
internal/domains/profile/service/entities.go
Normal file
@ -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
|
||||
}
|
||||
27
internal/domains/profile/service/getVacancies.go
Normal file
27
internal/domains/profile/service/getVacancies.go
Normal file
@ -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
|
||||
|
||||
}
|
||||
42
internal/domains/profile/service/saveResume.go
Normal file
42
internal/domains/profile/service/saveResume.go
Normal file
@ -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
|
||||
}
|
||||
39
internal/domains/profile/service/sendUserData.go
Normal file
39
internal/domains/profile/service/sendUserData.go
Normal file
@ -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
|
||||
}
|
||||
22
internal/domains/profile/service/service.go
Normal file
22
internal/domains/profile/service/service.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
34
internal/domains/profile/service/updateUser.go
Normal file
34
internal/domains/profile/service/updateUser.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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(
|
||||
"<b>Резюме</b>: %v\n<b>Зарплатная вилка</b>: %v\n<b>Должность</b>: %v\n<b>Опыт работы</b>: %v\n<b>Формат работы</b>: %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)
|
||||
}
|
||||
57
internal/infrastructure/grpcClient/client.go
Normal file
57
internal/infrastructure/grpcClient/client.go
Normal file
@ -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
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package httpClient
|
||||
|
||||
// ----------------------------------------
|
||||
// COMMON
|
||||
// ----------------------------------------
|
||||
|
||||
type loggerInstance interface {
|
||||
Info(string, ...any)
|
||||
Error(string, ...any)
|
||||
}
|
||||
1
migrations/000001_create_users_table.down.sql
Normal file
1
migrations/000001_create_users_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
12
migrations/000001_create_users_table.up.sql
Normal file
12
migrations/000001_create_users_table.up.sql
Normal file
@ -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()
|
||||
);
|
||||
11
pkg/minioDB/createBucket.go
Normal file
11
pkg/minioDB/createBucket.go
Normal file
@ -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{})
|
||||
}
|
||||
12
pkg/minioDB/existBucket.go
Normal file
12
pkg/minioDB/existBucket.go
Normal file
@ -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
|
||||
}
|
||||
16
pkg/minioDB/generateFileName.go
Normal file
16
pkg/minioDB/generateFileName.go
Normal file
@ -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
|
||||
}
|
||||
13
pkg/minioDB/get.go
Normal file
13
pkg/minioDB/get.go
Normal file
@ -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{})
|
||||
}
|
||||
11
pkg/minioDB/listBuckets.go
Normal file
11
pkg/minioDB/listBuckets.go
Normal file
@ -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)
|
||||
}
|
||||
36
pkg/minioDB/minio.go
Normal file
36
pkg/minioDB/minio.go
Normal file
@ -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
|
||||
}
|
||||
13
pkg/minioDB/remove.go
Normal file
13
pkg/minioDB/remove.go
Normal file
@ -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{})
|
||||
}
|
||||
48
pkg/minioDB/uploadFile.go
Normal file
48
pkg/minioDB/uploadFile.go
Normal file
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user