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