feat: Added main logic for telegram bot.

This commit is contained in:
Назар Гольченко 2025-10-13 17:33:11 +03:00
parent b61482bdd4
commit bad3c8f6c6
53 changed files with 1567 additions and 151 deletions

View File

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

View File

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

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

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

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

View File

@ -0,0 +1,9 @@
package constants
type apiKey string
const APIKey apiKey = "api-key"
func (t apiKey) String() string {
return string(APIKey)
}

View 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 = "Окей, помогу. Скину 23 релевантные роли уже сегодня и мини-план подготовки, пришли свое резюме"
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 = "При вычислении подходящих вакансий, возникли технические шоколадки)) Попробуй позже..."
)

View File

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

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

View File

@ -1,3 +0,0 @@
package constants
const ()

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

View File

@ -0,0 +1,8 @@
package constants
type ResponseStatus string
const (
SuccessStatus ResponseStatus = "success"
ErrorStatus ResponseStatus = "error"
)

View File

@ -0,0 +1,28 @@
package constants
type SalaryRange string
const (
FirstStepSalaryRange SalaryRange = "до 180k ₽"
SecondStepSalaryRange SalaryRange = "180300k ₽"
ThirdStepSalaryRange SalaryRange = "300450k ₽"
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)
}

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

View File

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

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

View 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")
}
}

View File

@ -0,0 +1,5 @@
package utils
func ValidateWorkExperience(val *string) bool {
return val == nil || len(*val) > 100
}

View File

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

View File

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

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

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

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

View File

@ -0,0 +1,11 @@
package profileRepository
type repository struct {
pgDB pgDBInstance
}
func New(pgDBInstance pgDBInstance) *repository {
return &repository{
pgDB: pgDBInstance,
}
}

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

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

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

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

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

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

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

View 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,
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
package httpClient
// ----------------------------------------
// COMMON
// ----------------------------------------
type loggerInstance interface {
Info(string, ...any)
Error(string, ...any)
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View 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()
);

View 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{})
}

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

View 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
View 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{})
}

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